10176 lines
526 KiB
HTML
10176 lines
526 KiB
HTML
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
<title>HTML Tower Defense</title>
|
|
|
|
<script src="assets/towers.js?v=2"></script>
|
|
<script src="assets/intel.js"></script>
|
|
|
|
<style>
|
|
|
|
/*
|
|
==========================================================================
|
|
MASTER AI DEVELOPMENT ROADMAP (UPDATED)
|
|
==========================================================================
|
|
|
|
✅ COMPLETED (Built into engine)
|
|
[1] [TOWER ANTI-AIR LIMITS] - Towers 1, 2, and 3 ignore flyers.
|
|
[2] [BONUS BOUNTY UPGRADES] - bonus_bounty logic grants extra caps.
|
|
[3] [DYNAMIC TACTICAL TOOLTIPS] - Matchups (Strong vs/Weak vs) display.
|
|
[4] [FACTION UPGRADE PATHS] - All 40 towers have 4-tier upgrade arrays mapped out.
|
|
[11] [VICTORY / POST-GAME SCREEN] - Game over screen calculates MVP, kills, caps, accuracy.
|
|
[13] [UI QOL: CANCEL BARRAGE] - Right-click and Escape key cancel targeting modes.
|
|
[14] [DYNAMIC TOP-BAR INTEL BRIEF] - Announcer system operational and reading intel.js.
|
|
|
|
⚠️ PARTIALLY COMPLETED (Needs Finishing)
|
|
[9] [ADVANCED AUDIO MANAGEMENT] - Global sounds exist, but volume sliders are missing.
|
|
|
|
❌ STILL TO DO (Unfinished / Missing)
|
|
[5] [CUSTOM MAP EDITOR - FILE I/O] - Missing browser download for .wd files and file input to load.
|
|
[6] [WASTELAND ASSAULT MODE] - UI toggle exists, but core engine mechanics to flip roles are missing.
|
|
[7 & 8] [PERSISTENT SAVE/LOAD SYSTEM] - Lacks automatic localStorage system to save/load progress.
|
|
[10] [CURATED WAVE CONFIGURATIONS] - Missing custom wave scripter; relying on dynamic budget.
|
|
[12] [ASSET MANAGEMENT] - Missing sprite rendering; using basic Canvas geometry.
|
|
==========================================================================
|
|
*/
|
|
|
|
|
|
@font-face {
|
|
font-family: 'TTChocolates';
|
|
src: url('assets/TT Chocolates ExtraLight.otf') format('opentype');
|
|
font-weight: normal;
|
|
font-style: normal;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: 'Overseer';
|
|
src: url('assets/OverseerItalic.otf') format('opentype');
|
|
font-weight: normal;
|
|
font-style: normal;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
body, html { margin: 0; padding: 0; width: 100vw; height: 100vh; overflow: hidden; background: #000; font-family: 'Segoe UI', Tahoma, Verdana, sans-serif; }
|
|
|
|
#game-container { width: 100vw !important; height: 100vh !important; display: flex; flex-direction: column; }
|
|
|
|
/* Start Screen */
|
|
#start-screen { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 600px; max-width: 95vw; padding: 20px 40px 40px 40px; background-color: #2c3322; background-image: url('assets/images/green-dust-and-scratches.png'), url('assets/images/snow.png'), url('assets/images/white-wall-3-2.png'), url('assets/images/scratch.png'), url('assets/images/mooning.png'); background-repeat: repeat, repeat, repeat, repeat; background-blend-mode: overlay, overlay, overlay, overlay; border-radius: 5px; border: 3px solid #1a1d13; border-top-color: #6b7552; border-left-color: #6b7552; box-shadow: 0 15px 50px rgba(0,0,0,0.9), inset 0 0 20px rgba(0,0,0,0.6); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 1000; }
|
|
|
|
/* Title Neon Ignition Sequence (Yellow/White Bloom) */
|
|
@keyframes title-neon-flicker {
|
|
0%, 4.9% { color: #111111; text-shadow: none; }
|
|
5%, 6% { color: #f2d57e; text-shadow: 0 0 10px rgba(242, 213, 126, 0.8), 2px 2px 0px #000; }
|
|
6.1%, 12.9% { color: #111111; text-shadow: none; }
|
|
13%, 15% { color: #f2d57e; text-shadow: 0 0 15px rgba(242, 213, 126, 0.8), 0 0 5px #fff, 2px 2px 0px #000; }
|
|
15.1%, 16.9%{ color: #111111; text-shadow: none; }
|
|
17%, 18% { color: #aa8a33; text-shadow: 0 0 5px rgba(242, 213, 126, 0.8), 2px 2px 0px #000; }
|
|
18.1%, 24.9%{ color: #111111; text-shadow: none; }
|
|
25%, 28% { color: #f2d57e; text-shadow: 0 0 20px rgba(242, 213, 126, 0.8), 0 0 10px #fff, 2px 2px 0px #000; }
|
|
28.1%, 40% { color: #111111; text-shadow: none; }
|
|
100% { color: #f2d57e; text-shadow: 0 0 30px rgba(242, 213, 126, 0.8), 0 0 15px rgba(242, 213, 126, 0.5), 0 0 5px #fff, 2px 2px 0px #000; }
|
|
}
|
|
|
|
/* Base Animated Title */
|
|
#start-screen h1 {
|
|
position: relative; /* Needed to anchor the texture overlay */
|
|
font-size: clamp(30px, 8vw, 55px); color: #f2d57e; margin-top: 0; margin-bottom: 30px; text-transform: uppercase;
|
|
text-align: center; white-space: nowrap; /* Forces text to stay on one line and centered! */
|
|
text-shadow: 0 0 30px rgba(242, 213, 126, 0.8), 0 0 15px rgba(242, 213, 126, 0.5), 0 0 5px #fff, 2px 2px 0px #000;
|
|
font-family: 'Overseer', sans-serif; font-weight: normal; letter-spacing: 2px;
|
|
-webkit-text-stroke: 1px #000;
|
|
animation: title-neon-flicker 3.0s ease-in-out 1s both;
|
|
}
|
|
|
|
|
|
/* The Texture Layer perfectly overlaid on top of the lit text! */
|
|
#start-screen h1::after {
|
|
content: 'WASTELAND DEFENSE';
|
|
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
/* Using a darker base color to ensure the fibers cast a solid black shadow onto the yellow text */
|
|
background-color: #888;
|
|
background-image: url('assets/images/light-paper-fibers.png'); background-repeat: repeat;
|
|
-webkit-background-clip: text; color: transparent;
|
|
|
|
/* Multiply forces the dark fibers to permanently stain the bright yellow text below it */
|
|
mix-blend-mode: multiply;
|
|
pointer-events: none; -webkit-text-stroke: 0px transparent;
|
|
}
|
|
|
|
|
|
/* Mechanical Dropdowns */
|
|
/* NEW: Power-On and Active Animations */
|
|
@keyframes startup-flicker {
|
|
0% { opacity: 0; }
|
|
10% { opacity: 1; }
|
|
20% { opacity: 0; }
|
|
30% { opacity: 1; }
|
|
40% { opacity: 0; }
|
|
60% { opacity: 1; }
|
|
70% { opacity: 0.5; }
|
|
100% { opacity: 1; }
|
|
}
|
|
@keyframes active-blink {
|
|
0%, 30%, 60%, 100% {
|
|
background-color: var(--btn-on, #f2d57e); color: #222222;
|
|
box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 10px rgba(242, 213, 126, 0.4);
|
|
}
|
|
15%, 45% {
|
|
background-color: var(--btn-off, #e0dcc8); color: #444444;
|
|
box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 3px 3px 6px rgba(0,0,0,0.8);
|
|
}
|
|
}
|
|
|
|
/* ========================================= */
|
|
/* ISOLATED MAIN MENU STYLES & ANIMATIONS */
|
|
/* ========================================= */
|
|
|
|
/* 1. Main Menu Animations */
|
|
@keyframes mm-flicker-in {
|
|
0% { opacity: 0; } 20% { opacity: 0.8; } 40% { opacity: 0; } 60% { opacity: 1; } 80% { opacity: 0.5; } 100% { opacity: 1; }
|
|
}
|
|
@keyframes mm-flicker-in-lit {
|
|
/* OFF STATE: Hard cuts with uneven delays between sparks */
|
|
0%, 11%, 16%, 39%, 44%, 49%, 54%, 79%, 84%, 87% {
|
|
background-color: #666; color: #333; box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 2px 2px 5px rgba(0,0,0,0.9);
|
|
}
|
|
/* ON STATE: Violent, sharp electrical sputters before catching at 88% */
|
|
12%, 15%, 40%, 43%, 50%, 53%, 80%, 83%, 88%, 100% {
|
|
background-color: #f2d57e; color: #222; box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 10px rgba(242, 213, 126, 0.6);
|
|
}
|
|
}
|
|
@keyframes mm-fade-in-lit {
|
|
0% { background-color: #666; color: #333; box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 2px 2px 5px rgba(0,0,0,0.9); }
|
|
100% { background-color: #f2d57e; color: #222; box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 10px rgba(242, 213, 126, 0.6); }
|
|
}
|
|
@keyframes mm-special-blink {
|
|
/* Sharp, non-fading hardware blinks */
|
|
0%, 49% { background-color: #f2d57e; color: #222; box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 10px rgba(242, 213, 126, 0.6); }
|
|
50%, 99% { background-color: #666; color: #333; box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 2px 2px 5px rgba(0,0,0,0.9); }
|
|
100% { background-color: #f2d57e; color: #222; box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 10px rgba(242, 213, 126, 0.6); }
|
|
}
|
|
|
|
|
|
/* 2. Menu Headers */
|
|
.menu-header {
|
|
color: #aaa; font-weight: bold; font-size: 1.4vh; text-transform: uppercase;
|
|
margin-bottom: 5px; text-shadow: none; letter-spacing: 1px; line-height: 1;
|
|
}
|
|
|
|
|
|
/* 3. Base Physical Styling for all Menu Inputs */
|
|
.menu-select, #btn-start, .special-label {
|
|
appearance: none; -webkit-appearance: none; -moz-appearance: none;
|
|
position: relative; z-index: 1; overflow: visible; /* Changed to visible so glow can bleed out */
|
|
border: 2px solid #222; border-top: 2px solid #555; border-left: 2px solid #444; border-bottom: 3px solid #0a0a0a; border-right: 3px solid #111;
|
|
box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 2px 2px 5px rgba(0,0,0,0.9);
|
|
font-family: Arial, Helvetica, sans-serif; font-weight: bold; text-transform: uppercase; cursor: pointer;
|
|
transition: transform 0.05s, box-shadow 0.05s;
|
|
}
|
|
/* Active Click Physics */
|
|
.menu-select:active, #btn-start:active:not(:disabled), .special-label:active, .special-chk:checked + .special-label:active {
|
|
border-top: 3px solid #000 !important; border-left: 3px solid #000 !important; border-bottom: 2px solid #444 !important; border-right: 2px solid #444 !important;
|
|
box-shadow: inset 2px 2px 10px rgba(0,0,0,0.9), 0px 0px 0px rgba(0,0,0,0) !important; transform: translateY(2px) !important;
|
|
}
|
|
|
|
/* 4. Dropdowns */
|
|
.menu-select {
|
|
display: block; /* Removes Flexbox to fix text-wrapping bugs */
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
font-size: 1.8vh;
|
|
padding: 10px 35px; /* Equal left/right padding perfectly centers text between the edges */
|
|
width: 350px; max-width: 80%;
|
|
background-color: #f2d57e; color: #222222; text-shadow: none;
|
|
background-image: url('data:image/svg+xml;utf8,<svg fill="%23222" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/></svg>');
|
|
background-repeat: no-repeat;
|
|
background-position: right 10px center;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-select option {
|
|
background: #111111 !important;
|
|
color: #f2d57e !important;
|
|
text-shadow: none;
|
|
font-weight: bold;
|
|
}
|
|
/* Override browser highlight to keep background black, but change text color to green on hover/focus */
|
|
.menu-select option:checked,
|
|
.menu-select option:hover,
|
|
.menu-select:focus > option:checked {
|
|
background: #111111 linear-gradient(0deg, #111111 0%, #111111 100%) !important;
|
|
color: #55ff55 !important;
|
|
box-shadow: 0 0 10px 100px #111111 inset !important;
|
|
}
|
|
|
|
|
|
/* NEW: Custom Div Dropdown System */
|
|
.custom-select-wrapper {
|
|
position: relative;
|
|
width: 350px;
|
|
max-width: 80%;
|
|
margin: 0 auto 20px auto;
|
|
}
|
|
.custom-select-options {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background-color: #111111;
|
|
border: 2px solid #555;
|
|
z-index: 2000;
|
|
display: none;
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.9);
|
|
}
|
|
.custom-select-options.open {
|
|
display: block;
|
|
}
|
|
.custom-option {
|
|
padding: 10px;
|
|
color: #f2d57e;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
font-size: 1.8vh;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid #333;
|
|
}
|
|
.custom-option:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.custom-option:hover {
|
|
color: #55ff55;
|
|
}
|
|
|
|
/* Arrow for the currently selected option */
|
|
.custom-option.selected::before {
|
|
content: '► ';
|
|
margin-right: 5px;
|
|
/* Color property removed so it dynamically inherits yellow/green from the parent text */
|
|
}
|
|
|
|
|
|
/* 5. Play Button */
|
|
#btn-start {
|
|
margin-top: 0px; padding: 12px 40px; font-size: 2.2vh; border-radius: 3px;
|
|
/* Removed Courier New so it correctly matches the Arial UI buttons */
|
|
background-color: #666; color: #333; text-shadow: none; /* Dark unlit */
|
|
}
|
|
/* When JS applies the ready class, it stays lit */
|
|
#btn-start.menu-play-ready {
|
|
background-color: #f2d57e; color: #222222; text-shadow: none;
|
|
box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 10px rgba(242, 213, 126, 0.6);
|
|
/* Removed animation so it smoothly transitions without flickering */
|
|
}
|
|
|
|
|
|
/* 6. S.P.E.C.I.A.L. Buttons */
|
|
.special-chk { position: absolute; opacity: 0; pointer-events: none; }
|
|
|
|
/* Apply the animation permanently to ALL labels so their internal clocks are forever synced */
|
|
.special-label {
|
|
display: inline-block; width: 4vh; height: 4vh; line-height: 3.5vh; text-align: center;
|
|
margin: 0 4px 0px 4px;
|
|
font-size: 2vh; border-radius: 3px;
|
|
background-color: #666; color: #333; text-shadow: none;
|
|
vertical-align: top;
|
|
animation: mm-special-blink 0.5s infinite;
|
|
}
|
|
|
|
/* Checked State: Locks solid lit (Using !important to override the animation without stopping the clock) */
|
|
.special-chk:checked + .special-label {
|
|
background-color: #f2d57e !important; color: #222 !important; text-shadow: none !important;
|
|
box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 10px rgba(242, 213, 126, 0.6) !important;
|
|
}
|
|
|
|
/* Disabled State: Dark unpowered casing (Using !important to override the animation) */
|
|
.special-chk:disabled:not(:checked) + .special-label {
|
|
background-color: #666 !important; color: #333 !important;
|
|
border-top-color: #444 !important; border-left-color: #444 !important; border-bottom-color: #111 !important; border-right-color: #222 !important;
|
|
text-shadow: none !important; cursor: not-allowed !important; box-shadow: inset 0 0 10px rgba(0,0,0,0.8) !important;
|
|
}
|
|
|
|
/* Map Section (Now 81% height to make room for the Top Bar) */
|
|
#td-board { height: 81vh !important; width: 100vw !important; background: #1a1a1a; position: relative; display: none; overflow: hidden; }
|
|
.demo-blur { filter: blur(4px) brightness(0.6); }
|
|
#td-canvas { width: 100% !important; height: 100% !important; display: block; z-index: 1; pointer-events: auto; }
|
|
|
|
/* NEW: Environmental Effect Animations */
|
|
@keyframes env-pan-left { from { background-position: 0px 0px; } to { background-position: -2000px 0px; } }
|
|
@keyframes env-pan-right { from { background-position: 0px 0px; } to { background-position: 2000px 0px; } }
|
|
@keyframes env-pan-up { from { background-position: 0px 0px; } to { background-position: 0px -2000px; } }
|
|
@keyframes env-pan-down { from { background-position: 0px 0px; } to { background-position: 0px 2000px; } }
|
|
@keyframes env-fade { 0%, 100% { opacity: 0; } 50% { opacity: var(--env-opacity, 1.0); } }
|
|
|
|
/* NEW: Damage Screen Shake */
|
|
|
|
@keyframes damage-shake {
|
|
0% { transform: translate(1px, 1px) rotate(0deg); }
|
|
10% { transform: translate(-1px, -2px) rotate(-1deg); }
|
|
20% { transform: translate(-3px, 0px) rotate(1deg); }
|
|
30% { transform: translate(3px, 2px) rotate(0deg); }
|
|
40% { transform: translate(1px, -1px) rotate(1deg); }
|
|
50% { transform: translate(-1px, 2px) rotate(-1deg); }
|
|
60% { transform: translate(-3px, 1px) rotate(0deg); }
|
|
70% { transform: translate(3px, 1px) rotate(-1deg); }
|
|
80% { transform: translate(-1px, -1px) rotate(1deg); }
|
|
90% { transform: translate(1px, 2px) rotate(0deg); }
|
|
100% { transform: translate(1px, -2px) rotate(-1deg); }
|
|
}
|
|
.shake-active { animation: damage-shake 0.4s; }
|
|
|
|
|
|
|
|
/* Control Panel Section (15% height) */
|
|
#control-panel {
|
|
width: 100vw !important; height: 15vh !important; min-height: 15vh !important; max-height: 15vh !important;
|
|
background-color: #2c3322;
|
|
background-image:
|
|
url('assets/images/green-dust-and-scratches.png'),
|
|
url('assets/images/snow.png'),
|
|
url('assets/images/white-wall-3-2.png'),
|
|
url('assets/images/scratch.png'),
|
|
url('assets/images/mooning.png');
|
|
background-repeat: repeat, repeat, repeat, repeat;
|
|
background-blend-mode: overlay, overlay, overlay, overlay;
|
|
border-top: 3px solid #6b7552; border-bottom: none !important; /* Removed thick bottom border to gain space */
|
|
box-shadow: 0 -4px 15px rgba(0,0,0,0.9);
|
|
|
|
position: relative; z-index: 10;
|
|
display: none; padding: 0.5vh 1vw; box-sizing: border-box;
|
|
}
|
|
|
|
|
|
/* Authentic Nixie Tube Container */
|
|
|
|
.nixie-board { display: flex; gap: 4px; align-items: center; justify-content: flex-end; }
|
|
|
|
/* The Glass Vacuum Tube & Inner Gas Glow */
|
|
.nixie-tube {
|
|
display: inline-block; position: relative;
|
|
/* Soft ambient gas glow inside the glass */
|
|
background: radial-gradient(circle at center, rgba(180, 40, 0, 0.2) 0%, #030100 80%);
|
|
border-radius: 4px 4px 2px 2px; border: 1px solid #1a1a1a; border-bottom: 2px solid #000;
|
|
width: 1.4vw; height: 3.2vh;
|
|
box-shadow: inset 1px 1px 3px rgba(255,255,255,0.05), inset -2px -2px 5px rgba(0,0,0,0.9), 1px 1px 2px rgba(0,0,0,0.6);
|
|
/* Assigning the newly loaded TT Chocolates font */
|
|
font-family: 'TTChocolates', sans-serif; font-size: 2.8vh; font-weight: normal;
|
|
text-align: center; line-height: 3.2vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* The Hex Mesh & Glass Curve (Sits IN FRONT of everything) */
|
|
.nixie-tube::after {
|
|
content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
|
background:
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='14' viewBox='0 0 8 14'%3E%3Cpath d='M0 3.5L4 1.2l4 2.3v4.6L4 10.4L0 8.1V3.5z M4 10.4l4 2.3v4.6l-4 2.3l-4-2.3v-4.6L4 10.4z' fill='none' stroke='rgba(0,0,0,0.4)' stroke-width='0.8'/%3E%3C/svg%3E");
|
|
z-index: 5; pointer-events: none;
|
|
/* Removed the white linear-gradient to kill the gray smudge */
|
|
}
|
|
|
|
/* Individual Wire Filaments (Dormant State) */
|
|
.nixie-wire {
|
|
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
|
z-index: 1;
|
|
display: block; /* Removed flexbox for manual centering */
|
|
text-align: center;
|
|
line-height: 3.6vh; /* Pushes the text down to center it */
|
|
color: transparent;
|
|
opacity: 0; /* Hides the bulky outline blob completely */
|
|
transition: opacity 0.1s;
|
|
}
|
|
|
|
/* The Active Glowing Wire & 5 Random Flicker Profiles */
|
|
@keyframes nixie-flicker-1 {
|
|
0% { opacity: 0; } 10% { opacity: 0.8; } 15% { opacity: 0.1; } 25% { opacity: 0.9; } 30% { opacity: 0.2; } 45% { opacity: 0.6; } 100% { opacity: 1; }
|
|
}
|
|
@keyframes nixie-flicker-2 {
|
|
0% { opacity: 0; } 5% { opacity: 0.5; } 10% { opacity: 0; } 30% { opacity: 1; } 40% { opacity: 0.3; } 60% { opacity: 0.8; } 100% { opacity: 1; }
|
|
}
|
|
@keyframes nixie-flicker-3 {
|
|
0% { opacity: 0; } 20% { opacity: 1; } 25% { opacity: 0.2; } 35% { opacity: 0.7; } 50% { opacity: 0.1; } 65% { opacity: 0.9; } 100% { opacity: 1; }
|
|
}
|
|
@keyframes nixie-flicker-4 {
|
|
0% { opacity: 0; } 15% { opacity: 0.6; } 20% { opacity: 0.1; } 40% { opacity: 0.8; } 45% { opacity: 0.4; } 70% { opacity: 0.9; } 100% { opacity: 1; }
|
|
}
|
|
@keyframes nixie-flicker-5 {
|
|
0% { opacity: 0; } 8% { opacity: 1; } 18% { opacity: 0.2; } 28% { opacity: 0.9; } 38% { opacity: 0.3; } 58% { opacity: 0.8; } 100% { opacity: 1; }
|
|
}
|
|
|
|
.nixie-wire.warm-up-1 { animation: nixie-flicker-1 2.2s ease-in 0.4s both !important; }
|
|
.nixie-wire.warm-up-2 { animation: nixie-flicker-2 2.6s ease-in 0.6s both !important; }
|
|
.nixie-wire.warm-up-3 { animation: nixie-flicker-3 2.0s ease-in 0.3s both !important; }
|
|
.nixie-wire.warm-up-4 { animation: nixie-flicker-4 2.8s ease-in 0.8s both !important; }
|
|
.nixie-wire.warm-up-5 { animation: nixie-flicker-5 2.4s ease-in 0.5s both !important; }
|
|
|
|
|
|
.nixie-wire.active {
|
|
z-index: 3;
|
|
color: #fff3d4;
|
|
opacity: 1;
|
|
-webkit-text-stroke: 0px;
|
|
text-shadow:
|
|
0 0 2px #ffaa00,
|
|
0 0 4px #ff5500,
|
|
0 0 8px #ff2200,
|
|
0 0 15px #ff0000;
|
|
/* Animation removed from here, applied via JS classes instead */
|
|
}
|
|
|
|
|
|
|
|
/* Gas Ignition Flicker Animation */
|
|
@keyframes neon-flicker {
|
|
0% { opacity: 0.1; text-shadow: none; color: rgba(255,255,255,0.1); }
|
|
10% { opacity: 1.0; text-shadow: 0 0 10px #ff0000; }
|
|
20% { opacity: 0.2; text-shadow: none; color: rgba(255,255,255,0.2); }
|
|
30% { opacity: 0.8; text-shadow: 0 0 5px #ff5500; }
|
|
40% { opacity: 0.1; text-shadow: none; }
|
|
50% { opacity: 1.0; text-shadow: 0 0 2px #ffaa00, 0 0 5px #ff5500, 0 0 10px #ff2200; }
|
|
100% { opacity: 1.0; }
|
|
}
|
|
.nixie-wire.igniting { animation: neon-flicker 0.3s ease-out forwards; }
|
|
|
|
/* Raised Metal Plate for Static Symbols (%) */
|
|
.nixie-plate {
|
|
display: inline-block; background: linear-gradient(to bottom, #333, #222); color: #ffaa00;
|
|
font-family: 'TTChocolates', sans-serif; font-weight: bold; font-size: 2vh;
|
|
border-radius: 2px; border: 1px solid #1a1a1a; border-top: 2px solid #555; border-left: 2px solid #555;
|
|
box-shadow: 1px 1px 3px rgba(0,0,0,0.6); text-shadow: 1px 1px 0px #000;
|
|
width: 1.2vw; height: 3.2vh; text-align: center; line-height: 3vh; margin-left: 2px;
|
|
}
|
|
|
|
/* Critical Health Warning Pulse */
|
|
@keyframes critical-pulse { 0% { opacity: 0.4; } 100% { opacity: 1; } }
|
|
.nixie-tube.critical .nixie-wire.active {
|
|
color: rgba(255, 150, 150, 0.9);
|
|
text-shadow: 0 0 4px #ff0000, 0 0 10px #cc0000, 0 0 20px #880000;
|
|
animation: critical-pulse 0.5s infinite alternate;
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Layout for Panel */
|
|
.panel-col { display: flex; flex-direction: column; justify-content: flex-start; height: 100%; overflow: hidden; padding-top: 0; }
|
|
.panel-title { color: #aaa; font-weight: bold; font-size: 1.4vh; text-transform: uppercase; border-bottom: 1px solid #555; padding-bottom: 0.2vh; margin-top: 0; margin-bottom: 0.5vh; letter-spacing: 1px; line-height: 1; }
|
|
|
|
/* CRITICAL FIX: Shrunk font size and line height so it doesn't push off the screen */
|
|
.stat-line { color: #ddd; font-size: 1.4vh; margin-bottom: 0.2vh; line-height: 1.1; }
|
|
.highlight { color: #4caf50; font-weight: bold; }
|
|
|
|
/* Buttons - Forced Horizontal */
|
|
.build-grid { display: flex; flex-wrap: nowrap; gap: 0.5vw; width: 100%; justify-content: space-between; margin-top: 0.2vh; align-content: flex-start; }
|
|
|
|
/* Button Flicker Animations */
|
|
@keyframes flicker-on {
|
|
0% { opacity: 0.2; text-shadow: none; }
|
|
10% { opacity: 1; text-shadow: 0 0 8px #ff8800; }
|
|
20% { opacity: 0.2; text-shadow: none; }
|
|
30% { opacity: 1; text-shadow: 0 0 5px #ff8800; }
|
|
50% { opacity: 0.2; text-shadow: none; }
|
|
100% { opacity: 1; text-shadow: 0 0 8px #ff8800, 0 0 2px #fff; }
|
|
}
|
|
|
|
/* ---------------------------------------------------- */
|
|
/* DIRTY INCANDESCENT PUSH BUTTONS */
|
|
/* ---------------------------------------------------- */
|
|
/* Global CSS Variables for the Dashboard */
|
|
:root {
|
|
--btn-off: #666666; /* Dark unpowered hardware */
|
|
--btn-on: #f2d57e; /* Yellow backlit */
|
|
--power-level: 1; /* Defaults to full power */
|
|
}
|
|
|
|
/* Base Bezel & Structure (Always Off by default) */
|
|
.cmd-btn, .sys-btn {
|
|
position: relative;
|
|
background-color: var(--btn-off) !important;
|
|
|
|
/* ENGRAVED TEXT GLOBALLY */
|
|
color: #333333 !important;
|
|
text-shadow: none !important;
|
|
|
|
/* MODERATE CASING: Reduced from 4px back to 2px/3px */
|
|
border: 2px solid #222 !important;
|
|
border-top: 2px solid #555 !important;
|
|
border-left: 2px solid #444 !important;
|
|
border-bottom: 3px solid #0a0a0a !important;
|
|
border-right: 3px solid #111 !important;
|
|
|
|
height: 3.2vh; padding: 0 0.2vw;
|
|
display: flex !important; align-items: center !important; justify-content: center !important;
|
|
font-size: 1.2vh; font-family: Arial, Helvetica, sans-serif; font-weight: bold;
|
|
text-transform: uppercase; flex: 1; text-align: center; cursor: pointer !important; border-radius: 3px;
|
|
white-space: nowrap; text-overflow: ellipsis; /* Removed overflow: hidden to allow the glow to bleed out! */
|
|
min-width: 0; /* CRITICAL FIX: Stops Flexbox from expanding when text gets longer */
|
|
|
|
/* MODERATE DROP SHADOW */
|
|
box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 2px 2px 5px rgba(0,0,0,0.9) !important;
|
|
transition: transform 0.05s, box-shadow 0.05s !important;
|
|
opacity: 1 !important;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* The Hidden Backlight Layer */
|
|
.cmd-btn::before, .sys-btn::before {
|
|
content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
|
background-color: var(--btn-on);
|
|
/* Added the 10px outer glow here! */
|
|
box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 10px rgba(242, 213, 126, 0.6);
|
|
opacity: 0; /* Off by default */
|
|
z-index: -1;
|
|
transition: opacity 0.05s; /* Very fast transition for organic sputtering */
|
|
}
|
|
|
|
/* ENABLED STATE (Backlight ON) - Regulated by --power-level */
|
|
.cmd-btn:not(:disabled):not(.unlit)::before,
|
|
.sys-btn:not(:disabled):not(.unlit)::before {
|
|
opacity: var(--power-level);
|
|
}
|
|
.cmd-btn:not(:disabled):not(.unlit),
|
|
.sys-btn:not(:disabled):not(.unlit) {
|
|
color: #222222 !important;
|
|
text-shadow: none !important;
|
|
}
|
|
|
|
/* UNAVAILABLE / LOCKED / DISABLED STATE (Dark unpowered hardware) */
|
|
.cmd-btn:disabled, .sys-btn:disabled, .cmd-btn.unlit, .sys-btn.unlit {
|
|
background-color: #555 !important;
|
|
color: #222 !important;
|
|
-webkit-text-fill-color: #222 !important; /* Force override any weird inline colors */
|
|
border-top: 2px solid #444 !important;
|
|
border-left: 2px solid #444 !important;
|
|
border-bottom: 3px solid #111 !important;
|
|
border-right: 3px solid #222 !important;
|
|
text-shadow: none !important;
|
|
cursor: not-allowed !important;
|
|
box-shadow: inset 0 0 10px rgba(0,0,0,0.8) !important;
|
|
}
|
|
/* Completely kill the internal lightbulb if disabled */
|
|
.cmd-btn:disabled::before, .sys-btn:disabled::before, .cmd-btn.unlit::before, .sys-btn.unlit::before {
|
|
display: none !important;
|
|
}
|
|
|
|
/* GAME OVER STATE (Kill all power to buttons and Nixies) */
|
|
body.game-over-dead .cmd-btn, body.game-over-dead .sys-btn {
|
|
background-color: #333 !important;
|
|
color: #111 !important;
|
|
-webkit-text-fill-color: #111 !important; /* Force override any weird inline colors */
|
|
border-top: 2px solid #222 !important; border-left: 2px solid #222 !important; border-bottom: 3px solid #000 !important; border-right: 3px solid #111 !important;
|
|
box-shadow: inset 0 0 10px rgba(0,0,0,0.9) !important;
|
|
text-shadow: none !important;
|
|
pointer-events: none !important;
|
|
transition: all 2s ease-out !important;
|
|
}
|
|
body.game-over-dead .cmd-btn::before, body.game-over-dead .sys-btn::before {
|
|
opacity: 0 !important;
|
|
transition: opacity 2s ease-out !important;
|
|
}
|
|
|
|
/* Soft Emergency Breathing Animation */
|
|
@keyframes emergency-pulse {
|
|
0% { background-color: rgba(204, 68, 68, 0.2); box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 5px rgba(200, 50, 50, 0.2); }
|
|
50% { background-color: rgba(204, 68, 68, 0.9); box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 15px rgba(255, 50, 50, 0.8); }
|
|
100% { background-color: rgba(204, 68, 68, 0.2); box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 5px rgba(200, 50, 50, 0.2); }
|
|
}
|
|
|
|
/* EMERGENCY RED POWER (Keep Restart and Main Menu buttons alive and pulsing) */
|
|
body.game-over-dead #ui-btn-restart, body.game-over-dead #ui-btn-exit {
|
|
background-color: #2a1111 !important; /* Dark, muddy reddish-brown */
|
|
color: #222 !important;
|
|
-webkit-text-fill-color: #222 !important;
|
|
pointer-events: auto !important; /* Keep them clickable! */
|
|
transition: none !important; /* Prevents fighting with the fade-out */
|
|
}
|
|
body.game-over-dead #ui-btn-restart::before, body.game-over-dead #ui-btn-exit::before {
|
|
display: block !important; /* Ensures the lightbulb isn't hidden if disabled */
|
|
opacity: 1 !important; /* Force opacity on, letting the animation control the alpha colors */
|
|
animation: emergency-pulse 2s ease-in-out infinite !important;
|
|
transition: none !important;
|
|
}
|
|
|
|
@keyframes vats-breathe {
|
|
0%, 100% { opacity: 0.3; box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 5px rgba(242, 213, 126, 0.4); }
|
|
50% { opacity: 1.0; box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 20px rgba(242, 213, 126, 1.0); }
|
|
}
|
|
.vats-charged-glow { color: #222 !important; background-color: var(--btn-off) !important; text-shadow: none !important; }
|
|
.vats-charged-glow::before {
|
|
display: block !important;
|
|
animation: vats-breathe 2s ease-in-out infinite !important;
|
|
}
|
|
|
|
/* Fade out Nixie Tubes */
|
|
|
|
body.game-over-dead .nixie-wire.active {
|
|
color: #111 !important; text-shadow: none !important;
|
|
transition: color 2s ease-out, text-shadow 2s ease-out !important;
|
|
}
|
|
|
|
/* PRESSED STATE (Straight Inward) */
|
|
.cmd-btn:active:not(:disabled):not(.unlit), .sys-btn:active:not(:disabled):not(.unlit) {
|
|
border-top: 3px solid #000 !important;
|
|
border-left: 3px solid #000 !important;
|
|
border-bottom: 2px solid #444 !important;
|
|
border-right: 2px solid #444 !important;
|
|
/* Heavy inner shadow, completely killed outer drop shadow to mimic pressing flush into panel */
|
|
box-shadow: inset 2px 2px 10px rgba(0,0,0,0.9), 0px 0px 0px rgba(0,0,0,0) !important;
|
|
transform: scale(0.98) !important; /* Shrinks straight inward slightly */
|
|
}
|
|
|
|
|
|
/* ---------------------------------------------------- */
|
|
/* FLUORESCENT JAGGED FLICKER ANIMATIONS */
|
|
/* ---------------------------------------------------- */
|
|
@keyframes btn-flicker-1 { 0% { opacity: 0; } 10% { opacity: 0.8; } 12% { opacity: 0; } 25% { opacity: 1; } 28% { opacity: 0; } 45% { opacity: 0.9; } 48% { opacity: 0; } 60% { opacity: 1; } 100% { opacity: 1; } }
|
|
@keyframes btn-flicker-2 { 0% { opacity: 0; } 5% { opacity: 0.4; } 8% { opacity: 0; } 30% { opacity: 1; } 32% { opacity: 0; } 50% { opacity: 0.8; } 55% { opacity: 0; } 65% { opacity: 1; } 100% { opacity: 1; } }
|
|
@keyframes btn-flicker-3 { 0% { opacity: 0; } 15% { opacity: 1; } 18% { opacity: 0; } 35% { opacity: 0.9; } 38% { opacity: 0; } 55% { opacity: 0.6; } 58% { opacity: 0; } 70% { opacity: 1; } 100% { opacity: 1; } }
|
|
@keyframes btn-flicker-4 { 0% { opacity: 0; } 20% { opacity: 0.6; } 22% { opacity: 0; } 40% { opacity: 1; } 44% { opacity: 0; } 60% { opacity: 0.7; } 63% { opacity: 0; } 80% { opacity: 1; } 100% { opacity: 1; } }
|
|
@keyframes btn-flicker-5 { 0% { opacity: 0; } 8% { opacity: 1; } 12% { opacity: 0; } 28% { opacity: 0.9; } 32% { opacity: 0; } 48% { opacity: 0.5; } 52% { opacity: 0; } 68% { opacity: 1; } 100% { opacity: 1; } }
|
|
@keyframes btn-flicker-fade { 0% { opacity: 0; } 100% { opacity: 1; } }
|
|
|
|
.warm-up-1::before { animation: btn-flicker-1 0.6s step-end 0.1s both !important; }
|
|
.warm-up-2::before { animation: btn-flicker-2 0.7s step-end 0.15s both !important; }
|
|
.warm-up-3::before { animation: btn-flicker-3 0.55s step-end 0.05s both !important; }
|
|
.warm-up-4::before { animation: btn-flicker-4 0.75s step-end 0.2s both !important; }
|
|
.warm-up-5::before { animation: btn-flicker-5 0.65s step-end 0.1s both !important; }
|
|
.warm-up-fade::before { animation: btn-flicker-fade 1s ease-in both !important; }
|
|
|
|
/* --- NEW: Wasteland Guide Icon & Book Modal --- */
|
|
.guide-icon {
|
|
position: absolute; cursor: pointer; z-index: 1500;
|
|
filter: drop-shadow(3px 5px 5px rgba(0,0,0,0.8));
|
|
transition: transform 0.2s ease;
|
|
}
|
|
.guide-icon:hover { transform: scale(1.15) rotate(-3deg); }
|
|
|
|
/* Menu Icon: Bottom Right of the Menu Box */
|
|
#guide-icon-menu { bottom: 15px; right: 15px; width: 60px; height: auto; }
|
|
|
|
/* Game Icon: Top Right overlapping the Top Bar */
|
|
#guide-icon-game { top: 0.5vh; right: 2vw; width: 45px; height: auto; }
|
|
|
|
|
|
|
|
#guide-modal-overlay {
|
|
display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
|
|
background: rgba(0,0,0,0.8); z-index: 100000; justify-content: center; align-items: center;
|
|
}
|
|
#guide-book {
|
|
display: flex; width: 80vw; max-width: 900px; height: 75vh;
|
|
background-color: #d8ceb9; /* Old parchment color */
|
|
background-image: url('assets/images/scratch.png'); background-blend-mode: multiply;
|
|
border-radius: 5px; box-shadow: 0 20px 50px rgba(0,0,0,0.9), inset 0 0 40px rgba(100,80,50,0.5);
|
|
color: #2b2013; font-family: 'Courier New', Courier, monospace;
|
|
position: relative;
|
|
}
|
|
/* Book Binding (Spine shadow) */
|
|
#guide-book::after {
|
|
content: ''; position: absolute; left: 50%; top: 0; bottom: 0; width: 40px; transform: translateX(-50%);
|
|
background: linear-gradient(to right, rgba(0,0,0,0) 0%, rgba(0,0,0,0.2) 20%, rgba(0,0,0,0.3) 50%, rgba(0,0,0,0.2) 80%, rgba(0,0,0,0) 100%);
|
|
pointer-events: none;
|
|
}
|
|
.guide-page {
|
|
flex: 1; padding: 40px; overflow-y: auto; box-sizing: border-box; position: relative;
|
|
}
|
|
.guide-page h2 { text-transform: uppercase; font-family: 'Overseer', sans-serif; font-size: 3.5vh; border-bottom: 2px solid #2b2013; padding-bottom: 5px; margin-top:0; text-align: center; }
|
|
.guide-page h3 { text-transform: uppercase; margin-bottom: 5px; border-bottom: 1px dashed #555; display: inline-block;}
|
|
.guide-toc li { margin-bottom: 15px; font-weight: bold; cursor: pointer; text-decoration: underline; }
|
|
.guide-toc li:hover { color: #500; }
|
|
.close-guide { position: absolute; top: 15px; right: 20px; font-size: 35px; font-weight: bold; cursor: pointer; color: #2b2013; z-index: 10; }
|
|
|
|
/* NEW: Pause Menu Dimming & Flashing */
|
|
/* 1. Unpower all OTHER buttons */
|
|
body.is-paused .cmd-btn:not(#ui-btn-pause),
|
|
body.is-paused .sys-btn:not(#ui-btn-pause) {
|
|
background-color: #555 !important;
|
|
color: #222 !important;
|
|
-webkit-text-fill-color: #222 !important;
|
|
border-top: 2px solid #444 !important;
|
|
border-left: 2px solid #444 !important;
|
|
border-bottom: 3px solid #111 !important;
|
|
border-right: 3px solid #222 !important;
|
|
text-shadow: none !important;
|
|
box-shadow: inset 0 0 10px rgba(0,0,0,0.8) !important;
|
|
pointer-events: none !important;
|
|
}
|
|
|
|
/* 2. Turn off the hidden lightbulb for all OTHER buttons */
|
|
body.is-paused .cmd-btn:not(#ui-btn-pause)::before,
|
|
body.is-paused .sys-btn:not(#ui-btn-pause)::before {
|
|
display: none !important;
|
|
}
|
|
|
|
/* 3. The Flashing Animation for the Lightbulb (0.75s blink -> 1.5s total cycle) */
|
|
@keyframes pause-light-blink {
|
|
0%, 49.9% { opacity: 1; }
|
|
50%, 100% { opacity: 0; }
|
|
}
|
|
|
|
/* 4. Force the CONTINUE button to have the fully dark, shadowless base state */
|
|
body.is-paused #ui-btn-pause {
|
|
background-color: #555 !important;
|
|
color: #222 !important;
|
|
-webkit-text-fill-color: #222 !important;
|
|
border-top: 2px solid #444 !important;
|
|
border-left: 2px solid #444 !important;
|
|
border-bottom: 3px solid #111 !important;
|
|
border-right: 3px solid #222 !important;
|
|
box-shadow: inset 0 0 10px rgba(0,0,0,0.8) !important; /* Strips the outer drop shadow */
|
|
}
|
|
|
|
/* 5. Let the blinking lightbulb push its shadow past the 3px borders (5px offset) */
|
|
body.is-paused #ui-btn-pause::before {
|
|
display: block !important;
|
|
animation: pause-light-blink 1.5s infinite !important;
|
|
transition: none !important;
|
|
box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 10px rgba(242, 213, 126, 0.6), 5px 5px 5px rgba(0,0,0,0.9) !important;
|
|
}
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
<body id="tower-defense">
|
|
<div id="audio-preloads" style="display:none;">
|
|
<!-- Sound Effects -->
|
|
<audio id="snd_gun_light" src="assets/sounds/gun_light.mp3" preload="auto"></audio>
|
|
<audio id="snd_gun_heavy" src="assets/sounds/gun_heavy.mp3" preload="auto"></audio>
|
|
<audio id="snd_machine_gun" src="assets/sounds/machine_gun.mp3" preload="auto"></audio>
|
|
<audio id="snd_shotgun" src="assets/sounds/shotgun.mp3" preload="auto"></audio>
|
|
<audio id="snd_missile_launch" src="assets/sounds/missile_launch.mp3" preload="auto"></audio>
|
|
<audio id="snd_laser_fire" src="assets/sounds/laser_fire.mp3" preload="auto"></audio>
|
|
<audio id="snd_explosion" src="assets/sounds/explosion.mp3" preload="auto"></audio>
|
|
|
|
<!-- Background Music (Placeholders) -->
|
|
<audio id="bgm_menu" src="assets/music/menu_theme_placeholder.mp3" loop preload="auto"></audio>
|
|
<audio id="bgm_game" src="assets/music/battle_theme_placeholder.mp3" loop preload="auto"></audio>
|
|
</div>
|
|
|
|
|
|
<div id="game-container">
|
|
<div id="start-screen">
|
|
<h1>Wasteland Defense</h1>
|
|
|
|
<!-- Game Type Selector -->
|
|
<div class="menu-header" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerText='Game Mode\n\nChoose your game mode.\n- Campaign: 4 chapters, 15 waves each, permanent perks.\n- Endless: Infinite waves.\n- Custom Map: Build, save (.wd), and play your own sectors.\n- Assault: Reverse TD. Spawn units to destroy enemy bases.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">Select Game Mode</div>
|
|
<select id="gametype-select" class="menu-select" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerText='Game Mode\n\nChoose your game mode.\n- Campaign: 4 chapters, 15 waves each, permanent perks.\n- Endless: Infinite waves.\n- Custom Map: Build, save (.wd), and play your own sectors.\n- Assault: Reverse TD. Spawn units to destroy enemy bases.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">
|
|
<option value="campaign" selected>Campaign</option>
|
|
<option value="endless">Endless Survival</option>
|
|
<option value="custom">Custom Map Builder</option>
|
|
<option value="assault">Wasteland Assault</option>
|
|
</select>
|
|
|
|
<!-- Dynamic Map Selector (Hidden in Campaign by default) -->
|
|
<div id="map-selection-group" style="display: none; width: 100%; text-align: center;">
|
|
<div class="menu-header">SECTOR SELECTION</div>
|
|
<select id="level-select" class="menu-select">
|
|
<option value="scene_level_1">Holo-Map: Open Field</option>
|
|
<option value="scene_level_2">Holo-Map: The Wall</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Faction Selector (Always Shown) -->
|
|
<div class="menu-header" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerText='Faction\n\nSelect your faction. Each faction offers unique defensive towers and playstyles.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">Select Faction</div>
|
|
<select id="faction-select" class="menu-select" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerText='Faction\n\nSelect your faction. Each faction offers unique defensive towers and playstyles.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">
|
|
<option value="NCR" selected>New California Republic (NCR)</option>
|
|
<option value="BOS">Brotherhood of Steel</option>
|
|
<option value="MINUTEMEN">Minutemen</option>
|
|
<option value="RAIDERS">Raiders</option>
|
|
</select>
|
|
|
|
<!-- S.P.E.C.I.A.L. Loadout (Shown in Campaign by default) -->
|
|
<div id="special-selection-group" style="color: #4caf50; margin-bottom: 25px; text-align: center;">
|
|
<div class="menu-header" style="margin-bottom: 10px;">S.P.E.C.I.A.L. LOADOUT (Pick 3)</div>
|
|
|
|
<label style="position: relative; display: inline-block;" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerHTML='<b style=\'color:#f2d57e\'>STRENGTH</b><br>Passive: Towers deal +15% Damage.<br>Active: Massive 15-second combat boost.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">
|
|
<input type="checkbox" class="special-chk" value="S"><div class="special-label">S</div>
|
|
</label>
|
|
<label style="position: relative; display: inline-block;" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerHTML='<b style=\'color:#f2d57e\'>PERCEPTION</b><br>Passive: Towers gain +2 Range.<br>Active: 15s of infinite map-wide range.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">
|
|
<input type="checkbox" class="special-chk" value="P"><div class="special-label">P</div>
|
|
</label>
|
|
<label style="position: relative; display: inline-block;" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerHTML='<b style=\'color:#f2d57e\'>ENDURANCE</b><br>Passive: +50% Base Health/Lives.<br>Active: 15-second defensive boost.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">
|
|
<input type="checkbox" class="special-chk" value="E"><div class="special-label">E</div>
|
|
</label>
|
|
<label style="position: relative; display: inline-block;" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerHTML='<b style=\'color:#f2d57e\'>CHARISMA</b><br>Passive: Towers & Upgrades cost 15% less.<br>Active: Drops 15 Cap Stashes from the sky.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">
|
|
<input type="checkbox" class="special-chk" value="C"><div class="special-label">C</div>
|
|
</label>
|
|
<label style="position: relative; display: inline-block;" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerHTML='<b style=\'color:#f2d57e\'>INTELLIGENCE</b><br>Passive: Towers need 15% less XP to level.<br>Active: Instantly grants all towers 100 XP.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">
|
|
<input type="checkbox" class="special-chk" value="I"><div class="special-label">I</div>
|
|
</label>
|
|
<label style="position: relative; display: inline-block;" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerHTML='<b style=\'color:#f2d57e\'>AGILITY</b><br>Passive: +15% Fire Rate & Cooldown Speed.<br>Active: Instantly resets all consumable cooldowns.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">
|
|
<input type="checkbox" class="special-chk" value="A"><div class="special-label">A</div>
|
|
</label>
|
|
<label style="position: relative; display: inline-block;" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerHTML='<b style=\'color:#f2d57e\'>LUCK</b><br>Passive: Increased chance of loot drops.<br>Active: Drops 5 Lunchboxes randomly on the map.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">
|
|
<input type="checkbox" class="special-chk" value="L"><div class="special-label">L</div>
|
|
</label>
|
|
</div>
|
|
|
|
|
|
<!-- Difficulty Selector -->
|
|
<div class="menu-header" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerText='Difficulty\n\nSelect the threat level. Higher difficulties increase enemy health and numbers.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">Select Difficulty</div>
|
|
<select id="difficulty-select" class="menu-select" onmouseover="document.getElementById('tower-tooltip').style.display='block'; document.getElementById('tower-tooltip').innerText='Difficulty\n\nSelect the threat level. Higher difficulties increase enemy health and numbers.';" onmouseout="document.getElementById('tower-tooltip').style.display='none';">
|
|
<option value="0.5">Vaultie (Easy)</option>
|
|
<option value="1.0" selected>Wastelander (Normal)</option>
|
|
<option value="2.0">Survivalist (Hard)</option>
|
|
</select>
|
|
|
|
<div style="display: flex; gap: 10px; width: 350px; max-width: 80%; margin: 0 auto;">
|
|
<button id="btn-start" style="flex: 1; margin: 0; padding: 10px 0; font-size: 2.2vh;">Play Game</button>
|
|
<button id="btn-load-game" title="Load Game" style="flex: 0 0 60px; margin: 0; padding: 10px 0; display: flex; justify-content: center; align-items: center; border-radius: 3px; border: 2px solid #222; border-top: 2px solid #555; border-left: 2px solid #444; border-bottom: 3px solid #0a0a0a; border-right: 3px solid #111; background-color: #f2d57e; color: #222222; text-shadow: none; box-shadow: inset 0 0 12px rgba(0,0,0,0.6), inset 0 0 4px rgba(0,0,0,0.4), 0 0 10px rgba(242, 213, 126, 0.6); cursor: pointer; transition: transform 0.05s, box-shadow 0.05s;">
|
|
<svg width="2.5vh" height="2.5vh" viewBox="0 0 24 24" fill="none" stroke="#222" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
|
|
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
|
<polyline points="7 3 7 8 15 8"></polyline>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<input type="file" id="input-load-game" accept=".json" style="display: none;">
|
|
<!-- Menu Guide Icon -->
|
|
|
|
<img id="guide-icon-menu" class="guide-icon" src="assets/images/guide.png" alt="Survival Guide" title="Read the Survival Guide" onclick="document.getElementById('guide-modal-overlay').style.display='flex';" />
|
|
</div>
|
|
|
|
|
|
|
|
<!-- NEW SOLID TOP INFORMATION BAR -->
|
|
<div id="top-bar" style="height: 4vh; background-color: #2c3322; background-image: url('assets/images/green-dust-and-scratches.png'), url('assets/images/snow.png'), url('assets/images/scratch.png'), url('assets/images/white-wall-3-2.png'), url('assets/images/mooning.png'); background-repeat: repeat, repeat, repeat, repeat; background-blend-mode: overlay, overlay, overlay, overlay; border-top: 2px solid #6a7354; border-bottom: 3px solid #1a1d13; box-shadow: 0 5px 15px rgba(0,0,0,0.8); display: none; justify-content: center; align-items: center; padding: 0 2vw; overflow: hidden; position: relative; z-index: 10;">
|
|
|
|
<div id="ui-intel-brief" style="width: 100%; position: relative; color: #d1d8b6; font-family: 'Courier New', Courier, monospace; font-size: 2.2vh; font-weight: bold; letter-spacing: 1px; white-space: nowrap; text-shadow: 1px 1px 3px #000; display: flex; align-items: center; justify-content: center; height: 100%;">
|
|
|
|
<!-- UNIFIED INTEL STATE -->
|
|
<div id="intel-state-wave" style="position: absolute; width: 100%; height: 100%; left: 0; display: flex; align-items: center; transition: opacity 0.5s;">
|
|
<span id="intel-wave-label" style="color: #ffcc00; padding: 0 10px; z-index: 5;">WAVE 0:</span>
|
|
|
|
<!-- Custom Live Combat Log -->
|
|
<div id="intel-ticker-container" style="flex: 1; position: relative; padding-left: 15px; height: 100%; display: flex; align-items: center;">
|
|
<div id="intel-ticker-text" style="color: #e0e0e0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;">[AWAITING COMBAT TELEMETRY...]</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
<div id="td-board" style="display: flex; flex-direction: column; flex-grow: 1; position: relative;">
|
|
|
|
<!-- NEW: Environmental Effect Layer -->
|
|
<div id="env-effect-overlay" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 10; pointer-events: none; background-repeat: repeat; display: none;"></div>
|
|
|
|
<!-- NEW: Screen Crack Overlays (Staggered Placements) -->
|
|
<img id="damage-overlay-70" src="assets/images/crushed_6813104.png" style="position: absolute; width: 35%; height: auto; pointer-events: none; z-index: 50; opacity: 0;" />
|
|
<img id="damage-overlay-50" src="assets/images/crushed_6813104.png" style="position: absolute; width: 35%; height: auto; pointer-events: none; z-index: 51; opacity: 0;" />
|
|
<img id="damage-overlay-30" src="assets/images/crushed_6813104.png" style="position: absolute; width: 35%; height: auto; pointer-events: none; z-index: 52; opacity: 0;" />
|
|
<img id="damage-overlay-10" src="assets/images/crushed_6813104.png" style="position: absolute; width: 35%; height: auto; pointer-events: none; z-index: 53; opacity: 0;" />
|
|
|
|
<!-- NEW: V.A.T.S. Overlay -->
|
|
<div id="vats-overlay" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 255, 0, 0.15); pointer-events: none; z-index: 55; display: none; box-shadow: inset 0 0 100px rgba(0, 255, 0, 0.5); mix-blend-mode: overlay;"></div>
|
|
|
|
<!-- NEW: Static Noise Signal Lost Layer -->
|
|
<div id="signal-lost-overlay" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: url('assets/images/noise.gif'); background-repeat: repeat; z-index: 54; display: none; pointer-events: none; mix-blend-mode: hard-light; opacity: 0.9;"></div>
|
|
|
|
<!-- NEW: HTML Game Over Text (Classic Terminal Style) -->
|
|
<div id="game-over-text" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 60; display: none; pointer-events: none; text-align: center; background-color: rgba(0, 0, 0, 0.9); padding: 40px 80px; border: 2px solid #4caf50; border-radius: 4px; box-shadow: 0 0 20px rgba(76, 175, 80, 0.4);">
|
|
<h1 style="font-family: 'Courier New', monospace; font-size: 6vh; color: #4caf50; text-shadow: 0 0 10px rgba(76,175,80,0.8); margin: 0; letter-spacing: 2px; -webkit-text-stroke: 0px;">CONNECTION LOST</h1>
|
|
|
|
<!-- POST-GAME STATS -->
|
|
<div style="margin-top: 25px; font-family: 'Courier New', monospace; font-size: 2.2vh; color: #aaffaa; text-align: left; display: inline-block; padding: 15px 30px; border: 1px dashed #4caf50; background: rgba(0,20,0,0.5);">
|
|
<div style="margin-bottom: 5px;">WAVES SURVIVED:<span id="go-stat-waves" style="float: right; font-weight: bold; color: #fff; margin-left: 30px;">0</span></div>
|
|
<div style="margin-bottom: 5px;">TOTAL SCORE:<span id="go-stat-score" style="float: right; font-weight: bold; color: #fff; margin-left: 30px;">0</span></div>
|
|
<div style="margin-bottom: 5px;">CAPS ACQUIRED:<span id="go-stat-caps" style="float: right; font-weight: bold; color: #fff; margin-left: 30px;">0</span></div>
|
|
<div style="margin-bottom: 5px;">SHOTS FIRED:<span id="go-stat-shots" style="float: right; font-weight: bold; color: #fff; margin-left: 30px;">0</span></div>
|
|
<div>MVP TOWER:<span id="go-stat-mvp" style="float: right; font-weight: bold; color: #ffcc00; margin-left: 30px;">NONE</span></div>
|
|
</div>
|
|
|
|
|
|
<p style="font-family: 'Courier New', monospace; font-size: 2.5vh; color: #4caf50; text-shadow: none; margin-top: 25px; font-weight: bold;" class="blink-text">> REBOOT SYSTEM CONSOLE_</p>
|
|
</div>
|
|
|
|
|
|
<canvas id="td-canvas" width="1920" height="900" style="width: 100%; height: 100%; cursor: default;">Sorry, your browser does not support the HTML 5 Canvas tag.</canvas>
|
|
</div>
|
|
|
|
|
|
|
|
<div id="control-panel" style="display: flex; flex-direction: column;">
|
|
|
|
<!-- NEW: OVER-BEZEL HUD ELEMENT -->
|
|
<div id="ui-speed-readout" style="display: none; position: absolute; top: -2.5vh; right: 2vw; color: #ffcc00; font-family: 'Courier New', monospace; font-weight: bold; font-size: 2vh; text-shadow: 0 0 5px #ffaa00, 2px 2px 2px #000; z-index: 100;">
|
|
TIME DILATION: <span id="ui-speed-val" class="blink-text">>> 1X</span>
|
|
</div>
|
|
|
|
<!-- Bottom 5 Columns -->
|
|
<div style="display: flex; width: 100%; height: 100%; gap: 1vw; padding-top: 0.5vh;">
|
|
|
|
<!-- Col 1: System Status -->
|
|
<div class="panel-col" style="flex: 1.4; border-right: 2px solid #444; padding-right: 1vw;">
|
|
<div class="panel-title">SYSTEM STATUS</div>
|
|
<div class="stat-line" style="display: flex; justify-content: flex-start; align-items: center; margin-bottom: 0.5vh;">
|
|
<span style="font-weight: bold; color: #888; width: 3.5vw; display: inline-block;">CAPS</span>
|
|
<div id="ui-money-container" class="nixie-board" style="flex: 1; justify-content: flex-end;"></div>
|
|
</div>
|
|
<div class="stat-line" style="display: flex; justify-content: flex-start; align-items: center; margin-bottom: 0.5vh;">
|
|
<span style="font-weight: bold; color: #888; width: 3.5vw; display: inline-block;">SCORE</span>
|
|
<div id="ui-score-container" class="nixie-board" style="flex: 1; justify-content: flex-end;"></div>
|
|
</div>
|
|
<div class="stat-line" style="display: flex; justify-content: flex-start; align-items: center; margin-bottom: 0.5vh;">
|
|
<span id="ui-health-label" style="font-weight: bold; color: #888; width: 3.5vw; display: inline-block;">HEALTH</span>
|
|
<div id="ui-life-container" class="nixie-board" style="flex: 1; justify-content: flex-end;"></div>
|
|
</div>
|
|
<div class="stat-line" style="display: flex; justify-content: flex-start; align-items: center; margin-bottom: 0.5vh;">
|
|
<span style="font-weight: bold; color: #888; width: 3.5vw; display: inline-block;">WAVE</span>
|
|
<div id="ui-wave-container" class="nixie-board" style="flex: 1; justify-content: flex-end;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Col 2: The Arsenal (Narrowed to flex 4, 4x3 Grid) -->
|
|
<div class="panel-col" style="flex: 3.8; border-right: 2px solid #444; padding-right: 1vw;">
|
|
<div class="panel-title">THE ARSENAL</div>
|
|
<div class="build-grid" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5vh 0.5vw;">
|
|
<!-- Row 1 -->
|
|
|
|
<button id="btn-buy-tower-1" class="cmd-btn" style="flex: 1 1 22%;">TOWER 1</button>
|
|
<button id="btn-buy-tower-2" class="cmd-btn" style="flex: 1 1 22%;">TOWER 2</button>
|
|
<button id="btn-buy-tower-3" class="cmd-btn" style="flex: 1 1 22%;">TOWER 3</button>
|
|
<button id="btn-buy-tower-4" class="cmd-btn" style="flex: 1 1 22%;">TOWER 4</button>
|
|
|
|
<!-- Row 2 -->
|
|
<button id="btn-buy-tower-5" class="cmd-btn" style="flex: 1 1 22%; color: #888;" disabled>\[UNLOCK\]</button>
|
|
<button id="btn-buy-tower-6" class="cmd-btn" style="flex: 1 1 22%; color: #888;" disabled>\[UNLOCK\]</button>
|
|
<button id="btn-buy-tower-7" class="cmd-btn" style="flex: 1 1 22%; color: #888;" disabled>\[UNLOCK\]</button>
|
|
<button id="btn-buy-tower-8" class="cmd-btn" style="flex: 1 1 22%; color: #888;" disabled>\[UNLOCK\]</button>
|
|
|
|
<!-- Row 3 (Pushed down from rows 1 & 2) -->
|
|
<button id="btn-buy-tower-9" class="cmd-btn" style="flex: 1 1 22%; color: #888;" disabled>\[UNLOCK\]</button>
|
|
<button id="btn-buy-tower-10" class="cmd-btn" style="flex: 1 1 22%; color: #888;" disabled>\[UNLOCK\]</button>
|
|
<button id="btn-buy-tower-11" class="cmd-btn" style="flex: 1 1 22%;" title="Deploys a Mr. Handy to collect loot automatically. (Max 1)">MR. HANDY</button>
|
|
<div style="flex: 1 1 22%;"></div><!-- Spacer -->
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Col 3: Consumables & S.P.E.C.I.A.L. -->
|
|
<div class="panel-col" style="flex: 2.0; border-right: 2px solid #444; padding-right: 1vw;">
|
|
<div class="panel-title">CONSUMABLES & S.P.E.C.I.A.L.</div>
|
|
<div class="build-grid" style="flex-wrap: wrap; gap: 0.5vh 0.5vw;">
|
|
<button id="btn-item-artillery" class="cmd-btn" style="flex: 1 1 30%;" title="Calls down a massive explosive strike." disabled>ARTILLERY</button>
|
|
<button id="btn-item-stimpak" class="cmd-btn" style="flex: 1 1 30%;" title="Heals 15 HP instantly (Max 100)." disabled>STIMPAK</button>
|
|
<button id="btn-item-psycho" class="cmd-btn" style="flex: 1 1 30%;" title="+25% Damage & Fire Rate for 10s." disabled>PSYCHO</button>
|
|
<button id="btn-item-cryomine" class="cmd-btn" style="flex: 1 1 30%;" title="Freezes and slows enemies in a radius." disabled>CRYO MINE</button>
|
|
<button id="btn-item-capmine" class="cmd-btn" style="flex: 1 1 30%;" title="Explodes into scattered Bottlecaps!" disabled>CAP MINE</button>
|
|
<button id="btn-item-blank" class="cmd-btn" style="flex: 1 1 30%; color: #888;" disabled>???</button>
|
|
</div>
|
|
<div class="build-grid" style="flex-wrap: nowrap; gap: 0.2vw; margin-top: 0.5vh;">
|
|
<button id="btn-special-s" class="cmd-btn sys-btn" style="flex: 1; background: #333; color: #888;" disabled>S</button>
|
|
<button id="btn-special-p" class="cmd-btn sys-btn" style="flex: 1; background: #333; color: #888;" disabled>P</button>
|
|
<button id="btn-special-e" class="cmd-btn sys-btn" style="flex: 1; background: #333; color: #888;" disabled>E</button>
|
|
<button id="btn-special-c" class="cmd-btn sys-btn" style="flex: 1; background: #333; color: #888;" disabled>C</button>
|
|
<button id="btn-special-i" class="cmd-btn sys-btn" style="flex: 1; background: #333; color: #888;" disabled>I</button>
|
|
<button id="btn-special-a" class="cmd-btn sys-btn" style="flex: 1; background: #333; color: #888;" disabled>A</button>
|
|
<button id="btn-special-l" class="cmd-btn sys-btn" style="flex: 1; background: #333; color: #888;" disabled>L</button>
|
|
</div>
|
|
|
|
<!-- Target Actions -->
|
|
<div id="target-actions-view" style="display: flex; gap: 0.5vw; margin-top: 0.5vh;">
|
|
<button id="ui-btn-upgrade" class="cmd-btn sys-btn" style="display:none; background: #2a7a2a;">UPGRADE</button>
|
|
<button id="ui-btn-sell" class="cmd-btn sys-btn" style="display:none; background: #7a2a2a;">SELL</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Col 4: NEW V.A.T.S. PANEL -->
|
|
<div class="panel-col" style="flex: 0.8; border-right: 2px solid #444; padding-right: 1vw;">
|
|
<div class="panel-title">V.A.T.S. MODULE</div>
|
|
<div id="vats-container" style="display: flex; flex-direction: column; justify-content: center; height: 100%; padding-bottom: 0.5vh;">
|
|
<!-- Removed the 'unlit' override class and background style to let standard disabled CSS take over -->
|
|
<button id="btn-vats-activate" class="cmd-btn" style="flex: 1; font-size: 1.8vh;" disabled>V.A.T.S.</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Col 5: System Controls -->
|
|
<div class="panel-col" style="flex: 2.0;">
|
|
<div class="panel-title">SYSTEM CONTROLS</div>
|
|
<div class="build-grid" style="flex-wrap: wrap; gap: 0.5vh 0.5vw; align-content: flex-start;">
|
|
<!-- Top Row -->
|
|
<button id="ui-btn-start-wave" class="cmd-btn sys-btn" style="flex: 1 1 30%; padding: 0.5vh 0;">START WAVE</button>
|
|
<button id="ui-btn-restart" class="cmd-btn sys-btn" style="flex: 1 1 30%; background: #555; padding: 0.5vh 0;">RESTART</button>
|
|
<button id="ui-btn-exit" class="cmd-btn sys-btn" style="flex: 1 1 30%; background: #555; padding: 0.5vh 0;">MAIN MENU</button>
|
|
|
|
<!-- Middle Row -->
|
|
<button id="ui-btn-slow" class="cmd-btn sys-btn" style="flex: 1 1 30%; padding: 0.5vh 0;">SLOW DOWN</button>
|
|
<button id="ui-btn-pause" class="cmd-btn sys-btn" style="flex: 1 1 30%; padding: 0.5vh 0;">PAUSE</button>
|
|
<button id="ui-btn-speed" class="cmd-btn sys-btn" style="flex: 1 1 30%; padding: 0.5vh 0;">SPEED UP</button>
|
|
|
|
<!-- Bottom Row: Audio Toggles & Save -->
|
|
<button id="ui-btn-sound" class="cmd-btn sys-btn" style="flex: 1 1 22%; padding: 0.5vh 0;">SOUND</button>
|
|
<button id="ui-btn-music" class="cmd-btn sys-btn" style="flex: 1 1 22%; padding: 0.5vh 0;">MUSIC</button>
|
|
<button id="ui-btn-save" class="cmd-btn sys-btn" style="flex: 1 1 22%; padding: 0.5vh 0;">SAVE</button>
|
|
<!-- NEW: Guide Button -->
|
|
<button id="ui-btn-guide" class="cmd-btn sys-btn" style="flex: 1 1 22%; padding: 0.5vh 0;" onclick="document.getElementById('guide-modal-overlay').style.display='flex';">GUIDE</button>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ADMIN PANEL -->
|
|
<style>
|
|
/* Compress Admin Panel to fit all tools inside 15vh */
|
|
#admin-panel .cmd-btn, #admin-panel .sys-btn { height: 2.2vh !important; font-size: 1vh !important; line-height: 1 !important; border-bottom: 2px solid #0a0a0a !important; border-right: 2px solid #111 !important; }
|
|
#admin-panel input, #admin-panel select { height: 2.2vh !important; font-size: 1vh !important; padding: 0 4px !important; box-sizing: border-box; }
|
|
#admin-panel .panel-title { font-size: 1.1vh !important; margin-bottom: 2px !important; padding-bottom: 0 !important; }
|
|
</style>
|
|
<div id="admin-panel" style="display: none; flex-direction: column; width: 100vw !important; height: 15vh !important; min-height: 15vh !important; max-height: 15vh !important; background: linear-gradient(to bottom, #422, #111); border-top: 4px solid #844; padding: 0.5vh 1vw; box-sizing: border-box; overflow: hidden;">
|
|
<div style="display: flex; width: 100%; height: 100%; gap: 1vw; padding-top: 0;">
|
|
|
|
<!-- Admin Col 1: Data & Image Loader (25%) -->
|
|
<div class="panel-col" style="flex: 1; border-right: 2px solid #533; padding-right: 1vw;">
|
|
<div class="panel-title" style="color: #faa; border-bottom-color: #844;">1. DATA & IMAGE LOADER</div>
|
|
<div style="display: flex; flex-direction: column; gap: 0.5vh; margin-bottom: 0.5vh;">
|
|
<div style="display: flex; gap: 0.5vw;">
|
|
<select id="admin-level-select" style="flex: 1; padding: 0.5vh; background: #222; color: #fff; border: 1px solid #666; font-size: 1.2vh; font-family: 'Courier New', monospace;"></select>
|
|
<button id="admin-btn-loadmap" class="cmd-btn" style="background: #642; flex: 1;">LOAD DATA</button>
|
|
</div>
|
|
<div style="display: flex; gap: 0.5vw;">
|
|
<input type="text" id="admin-img-input" placeholder="e.g. map03.jpg" style="padding: 0.5vh; background: #222; color: #fff; border: 1px solid #666; font-family: 'Courier New', monospace; font-size: 1.2vh; flex: 1;">
|
|
<button id="admin-btn-loadimg" class="cmd-btn" style="background: #642; flex: 1;">LOAD IMAGE</button>
|
|
</div>
|
|
|
|
<!-- NEW: Environmental Effects -->
|
|
<div style="border-top: 1px dashed #533; margin-top: 2px; padding-top: 4px; display: flex; flex-direction: column; gap: 0.5vh;">
|
|
<input type="text" id="admin-env-src" placeholder="Effect Image (e.g. fog.png)" style="padding: 0.5vh; background: #222; color: #fff; border: 1px solid #666; font-size: 1.2vh;">
|
|
<div style="display: flex; gap: 0.5vw; align-items: center;">
|
|
<select id="admin-env-type" style="flex: 1; padding: 0; background: #222; color: #fff; border: 1px solid #666; font-size: 1vh;">
|
|
<option value="static">Static</option>
|
|
<option value="fade">Fade</option>
|
|
<option value="pan_left">Left</option>
|
|
<option value="pan_right">Right</option>
|
|
<option value="pan_up">Up</option>
|
|
<option value="pan_down">Down</option>
|
|
</select>
|
|
<input type="number" id="admin-env-opacity" value="0.5" step="0.1" min="0" max="1" style="flex: 0.4; padding: 0; background: #222; color: #fff; border: 1px solid #666; font-size: 1vh; text-align: center;" title="Opacity">
|
|
<input type="number" id="admin-env-speed" value="20" min="1" style="flex: 0.4; padding: 0; background: #222; color: #fff; border: 1px solid #666; font-size: 1vh; text-align: center;" title="Speed">
|
|
<button id="admin-btn-env-apply" class="cmd-btn sys-btn" style="flex: 1; background: #466; margin: 0 !important;">APPLY</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Admin Col 2: G.E.C.K Editor (50%) -->
|
|
|
|
<div class="panel-col" style="flex: 2;">
|
|
<div class="panel-title" style="color: #faa; border-bottom-color: #844;">2. G.E.C.K. EDITOR OVERRIDE</div>
|
|
|
|
<div style="display: flex; gap: 1.5vw; height: 100%; margin-top: 0.5vh;">
|
|
<!-- Left Sub-Col: Painting Tools & Spawns -->
|
|
<div style="flex: 1; display: flex; flex-direction: column; gap: 0.5vh;">
|
|
<div style="display: flex; gap: 0.5vw;">
|
|
<button id="admin-brush-path" class="cmd-btn" style="flex: 1; background: #248;">PATH</button>
|
|
<button id="admin-brush-block" class="cmd-btn" style="flex: 1; background: #822;">BLOCK</button>
|
|
<button id="admin-brush-clear" class="cmd-btn" style="flex: 1; background: #282;">CLEAR</button>
|
|
</div>
|
|
<div style="display: flex; gap: 0.5vw;">
|
|
<button id="admin-brush-ent" class="cmd-btn" style="flex: 1; background: #aaa; color: #000;">ENTRANCE</button>
|
|
<button id="admin-brush-exit" class="cmd-btn" style="flex: 1; background: #aaa; color: #000;">EXIT</button>
|
|
</div>
|
|
<div style="display: flex; gap: 0.5vw; align-items: center; background: #222; padding: 0.2vh 0.5vw; border: 1px solid #444; border-radius: 3px;">
|
|
<span style="color: #faa; font-size: 1.2vh; font-weight: bold;">SPAWN MODE:</span>
|
|
<select id="admin-spawn-mode" style="flex: 1; padding: 0.5vh; background: transparent; color: #fff; border: none; font-size: 1.2vh; font-family: 'Courier New', monospace;">
|
|
<option value="random">Random Entrance</option>
|
|
<option value="simultaneous">Simultaneous</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Sub-Col: Towers & Global Actions -->
|
|
<div style="flex: 1; display: flex; flex-direction: column; gap: 0.5vh;">
|
|
<div style="display: flex; gap: 0.5vw; align-items: center;">
|
|
<select id="admin-tower-select" style="flex: 2; padding: 0.5vh; background: #222; color: #fff; border: 1px solid #666; font-size: 1.2vh; font-family: 'Courier New', monospace;">
|
|
<!-- Populated dynamically by JS -->
|
|
</select>
|
|
|
|
<input type="number" id="admin-tower-level" value="1" min="1" max="10" style="flex: 0.5; padding: 0.5vh; background: #222; color: #fff; border: 1px solid #666; font-size: 1.2vh; text-align: center;" title="Lvl">
|
|
<button id="admin-brush-tower" class="cmd-btn" style="flex: 1.5; background: #862;">PLACE</button>
|
|
</div>
|
|
<div style="display: flex; gap: 0.5vw;">
|
|
<button id="admin-btn-fill" class="cmd-btn" style="flex: 1; background: #642;">FILL GRID</button>
|
|
<button id="admin-btn-clearall" class="cmd-btn" style="flex: 1; background: #422;">CLEAR ALL</button>
|
|
</div>
|
|
<div style="display: flex; gap: 0.5vw;">
|
|
<button id="admin-btn-toggle" class="cmd-btn" style="flex: 1; background: #444;">OVERLAY</button>
|
|
<button id="admin-btn-open-assets" class="cmd-btn sys-btn" style="flex: 1; background: #622;">ASSETS</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Admin Col 3: Exporter with Text Box (25%) -->
|
|
<div class="panel-col" style="flex: 1; border-left: 2px solid #533; padding-left: 1vw;">
|
|
<div class="panel-title" style="color: #faa; border-bottom-color: #844;">3. EXPORT DATA</div>
|
|
<textarea id="admin-export-out" style="width: 100%; flex-grow: 1; background: #111; color: #0f0; font-family: monospace; font-size: 1vh; resize: none; border: 1px solid #444; margin-bottom: 0.5vh;" readonly placeholder="Code will generate here..."></textarea>
|
|
<div style="display: flex; gap: 0.5vw;">
|
|
<button id="admin-btn-validate" class="cmd-btn sys-btn" style="background: #288; flex: 1;">VALIDATE</button>
|
|
<button id="admin-btn-export" class="cmd-btn" style="background: #642; flex: 1;">GENERATE</button>
|
|
<button id="admin-btn-copy" class="cmd-btn" style="background: #268; flex: 1;">COPY</button>
|
|
</div>
|
|
<button id="admin-btn-wave-editor" class="cmd-btn sys-btn" style="background: #626; width: 100%; margin-top: 0.5vh;" onclick="TD.populateWaveEditor(); document.getElementById('wave-editor-modal-overlay').style.display='flex';">WAVE EDITOR</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NEW: Wave Timeline Editor Modal -->
|
|
<div id="wave-editor-modal-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 100000; justify-content: center; align-items: center;">
|
|
<div style="background: #1a1a1a; color: #0f0; padding: 20px; border: 2px solid #0f0; border-radius: 8px; width: 900px; max-width: 95vw; height: 85vh; display: flex; flex-direction: column; font-family: 'Courier New', monospace; box-shadow: 0 0 20px rgba(0,255,0,0.3);">
|
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 2px dashed #0f0; padding-bottom: 10px; margin-bottom: 15px;">
|
|
<h2 style="margin: 0; font-size: 1.5em;">WAVE TIMELINE EDITOR</h2>
|
|
<button onclick="document.getElementById('wave-editor-modal-overlay').style.display='none';" style="background: transparent; border: none; color: #0f0; font-size: 2em; cursor: pointer; line-height: 0.5;">×</button>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 20px; flex: 1; overflow: hidden;">
|
|
<!-- Left: Wave Selector -->
|
|
<div style="width: 150px; display: flex; flex-direction: column; border-right: 1px dashed #0f0; padding-right: 15px;">
|
|
<label style="font-weight: bold; margin-bottom: 10px;">SELECT WAVE:</label>
|
|
<select id="wave-editor-select" size="15" style="background: #222; color: #0f0; border: 1px solid #0f0; padding: 5px; flex: 1; font-family: 'Courier New', monospace;"></select>
|
|
<div style="display: flex; gap: 5px; margin-top: 10px;">
|
|
<button id="we-btn-add-wave" class="cmd-btn sys-btn" style="flex: 1; background: #2a7a2a; border-color: #0f0;">+ ADD</button>
|
|
<button id="we-btn-del-wave" class="cmd-btn sys-btn" style="flex: 1; background: #7a2a2a; border-color: #f00; color: #fcc;">- DEL</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Middle/Right: Wave Settings -->
|
|
|
|
<div style="flex: 1; display: flex; flex-direction: column; overflow-y: auto; padding-right: 10px; gap: 20px;">
|
|
|
|
<!-- Story Event Config -->
|
|
<div style="background: #111; padding: 10px; border: 1px solid #333;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
|
<label style="font-weight: bold; color: #ffaa00; cursor: pointer;">
|
|
<input type="checkbox" id="we-story-enable" style="margin-right: 5px;"> SHOW STORY INTRO BEFORE WAVE
|
|
</label>
|
|
<div style="display: flex; align-items: center; gap: 5px;">
|
|
<span style="font-size: 0.9em; color: #aaa;">TARGET FACTION:</span>
|
|
<select id="we-story-faction" style="background: #222; color: #0f0; border: 1px solid #0f0; padding: 2px; font-family: 'Courier New', monospace;">
|
|
<option value="ALL">ALL (Default)</option>
|
|
<option value="NCR">NCR</option>
|
|
<option value="BOS">Brotherhood of Steel</option>
|
|
<option value="MINUTEMEN">Minutemen</option>
|
|
<option value="RAIDERS">Raiders</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<input type="text" id="we-story-title" placeholder="Event Title (e.g. INCOMING TRANSMISSION)" style="width: 100%; background: #222; color: #0f0; border: 1px solid #0f0; padding: 5px; font-family: 'Courier New', monospace; margin-bottom: 5px; box-sizing: border-box;">
|
|
<textarea id="we-story-desc" placeholder="Story text goes here..." style="width: 100%; height: 60px; background: #222; color: #0f0; border: 1px solid #0f0; padding: 5px; font-family: 'Courier New', monospace; resize: vertical; box-sizing: border-box;"></textarea>
|
|
</div>
|
|
|
|
|
|
<!-- Tower Status Config -->
|
|
<div style="background: #111; padding: 10px; border: 1px solid #333;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
|
<span style="font-weight: bold; color: #ffaa00;">TOWER AVAILABILITY</span>
|
|
<button id="we-btn-copy-prev" class="cmd-btn sys-btn" style="background: #2a7a2a; padding: 5px 15px;" title="Copies tower locks from the previous wave">USE PREV OPTIONS</button>
|
|
</div>
|
|
<div id="we-tower-list" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
|
<!-- Populated by JS -->
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Monster Spawns Config -->
|
|
<div style="background: #111; padding: 10px; border: 1px solid #333; flex: 1;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
|
<span style="font-weight: bold; color: #ffaa00;">MONSTER SPAWNS</span>
|
|
<button id="we-btn-add-spawn" class="cmd-btn sys-btn" style="background: #2a7a2a; padding: 5px 15px;">+ ADD SPAWN</button>
|
|
</div>
|
|
<div id="we-spawn-list" style="display: flex; flex-direction: column; gap: 5px;">
|
|
<!-- Populated by JS -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="text/javascript">
|
|
var _TD = {
|
|
|
|
a: [],
|
|
retina: window.devicePixelRatio || 1,
|
|
init: function (td_board, is_debug) {
|
|
delete this.init;
|
|
var i, TD = {
|
|
version: "0.1.17",
|
|
is_debug: !!is_debug,
|
|
is_paused: true,
|
|
width: 16,
|
|
height: 16,
|
|
show_monster_life: true,
|
|
fps: 0,
|
|
exp_fps: 24,
|
|
exp_fps_half: 12,
|
|
exp_fps_quarter: 6,
|
|
exp_fps_eighth: 4,
|
|
stage_data: {},
|
|
sound_enabled: true, // Default to true
|
|
music_enabled: true, // NEW: Default to true
|
|
current_bgm: null, // NEW: Track current background music
|
|
|
|
// --- NEW: TYPEWRITER COMBAT LOG HANDLER ---
|
|
typewriter_queue: [],
|
|
typewriter_timer: 0,
|
|
typewriter_target: "",
|
|
typewriter_current: "",
|
|
typewriter_pause: 0, // NEW: Reading delay timer
|
|
|
|
logEvent: function(msg) {
|
|
var tickerEl = document.getElementById("intel-ticker-text");
|
|
if (!tickerEl) return;
|
|
|
|
msg = msg.toUpperCase();
|
|
|
|
this.typewriter_target = msg;
|
|
this.typewriter_current = "";
|
|
tickerEl.innerText = "";
|
|
this.typewriter_timer = 0;
|
|
},
|
|
|
|
// --- NEW: SMART ANNOUNCER BRAIN ---
|
|
Announcer: {
|
|
queue: [],
|
|
add: function(msg, priority) {
|
|
priority = priority || 1; // 1 = Flavor, 3 = Action, 5 = Critical
|
|
for (var i=0; i<this.queue.length; i++) {
|
|
if (this.queue[i].msg === msg) return;
|
|
}
|
|
this.queue.push({ msg: msg, priority: priority });
|
|
this.queue.sort(function(a, b) { return a.priority - b.priority; });
|
|
},
|
|
update: function() {
|
|
// PREVENT overwriting if it's currently typing OR the player is still reading it!
|
|
if (TD.typewriter_current.length < TD.typewriter_target.length) return;
|
|
if (TD.typewriter_pause > 0) return;
|
|
|
|
if (this.queue.length > 0) {
|
|
var item = this.queue.pop();
|
|
TD.logEvent(item.msg);
|
|
|
|
// Calculate reading time: ~0.5 seconds per word
|
|
var wordCount = item.msg.split(" ").length;
|
|
TD.typewriter_pause = Math.floor(wordCount * (TD.exp_fps * 0.5));
|
|
} else {
|
|
// 1% chance per frame to drop flavor text when completely idle
|
|
if (window.IntelData && Math.random() > 0.99) {
|
|
var flavor = window.IntelData.generateChatter(TD.current_faction);
|
|
TD.logEvent(flavor);
|
|
var wordCount = flavor.split(" ").length;
|
|
TD.typewriter_pause = Math.floor(wordCount * (TD.exp_fps * 0.5));
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
// --- NEW: GLOBAL AUDIO HANDLER ---
|
|
playSound: function(sound_id) {
|
|
|
|
|
|
if (!TD.is_paused && TD.sound_enabled) {
|
|
var snd = document.getElementById(sound_id);
|
|
|
|
if (snd) {
|
|
// cloneNode allows the same sound to play multiple times concurrently
|
|
var clone = snd.cloneNode();
|
|
clone.volume = 0.5; // Default to 50% volume so it isn't deafening
|
|
clone.play().catch(function(e){ /* Ignore browser autoplay block errors */ });
|
|
}
|
|
}
|
|
},
|
|
|
|
// --- NEW: GLOBAL MUSIC HANDLER ---
|
|
playMusic: function(track_id) {
|
|
// Stop current music first
|
|
if (TD.current_bgm) {
|
|
TD.current_bgm.pause();
|
|
TD.current_bgm.currentTime = 0;
|
|
}
|
|
|
|
if (!TD.music_enabled) return;
|
|
|
|
var bgm = document.getElementById(track_id);
|
|
if (bgm) {
|
|
bgm.volume = 0.3; // Slightly quieter than SFX
|
|
bgm.play().catch(function(e){ console.log("Music blocked by browser autoplay policy."); });
|
|
TD.current_bgm = bgm;
|
|
}
|
|
},
|
|
|
|
stopMusic: function() {
|
|
if (TD.current_bgm) {
|
|
TD.current_bgm.pause();
|
|
TD.current_bgm.currentTime = 0;
|
|
TD.current_bgm = null;
|
|
}
|
|
},
|
|
|
|
defaultSettings: function () {
|
|
return {
|
|
step_time: 36,
|
|
grid_size: 60 * _TD.retina,
|
|
padding: 0,
|
|
global_speed: 0.2,
|
|
show_grid: false,
|
|
admin_mode: false, // Prevents the render crash
|
|
admin_brush: "path" // Default brush type
|
|
};
|
|
},
|
|
|
|
|
|
|
|
init: function (ob_board) {
|
|
this.obj_board = TD.lang.$e(ob_board);
|
|
this.canvas = this.obj_board.getElementsByTagName("canvas")[0];
|
|
if (!this.canvas.getContext) return;
|
|
this.ctx = this.canvas.getContext("2d");
|
|
this.monster_type_count = TD.getDefaultMonsterAttributes();
|
|
this.iframe = 0;
|
|
this.last_iframe_time = (new Date()).getTime();
|
|
this.fps = 0;
|
|
this.start();
|
|
},
|
|
|
|
|
|
|
|
// --- NEW: V.A.T.S. Core Functions ---
|
|
startVats: function() {
|
|
// Must be fully charged and not already running
|
|
var vatsPercent = Math.min(100, Math.floor((TD.vats_charge / TD.vats_max_charge) * 100));
|
|
if (vatsPercent < 100 || TD.vats_active) return;
|
|
|
|
// FORCE KILL any stuck UI elements before activating
|
|
if (TD.stage && TD.stage.current_act && TD.stage.current_act.current_scene && TD.stage.current_act.current_scene.panel) {
|
|
TD.stage.current_act.current_scene.panel.balloontip.hide();
|
|
TD.stage.current_act.current_scene.panel.balloontip.el = null;
|
|
}
|
|
|
|
TD.vats_active = true;
|
|
TD.vats_charge = 0; // Consume the charge
|
|
TD.vats_shots = 6;
|
|
TD.vats_timer = 10 * TD.exp_fps; // 10 Seconds
|
|
|
|
// Slow time drastically (grinding crawl)
|
|
TD.global_speed_backup = TD.global_speed;
|
|
TD.global_speed = 0.025; // CHANGED: 75% slower, ensuring a heavy slow-motion feel
|
|
|
|
// Visuals
|
|
document.getElementById("vats-overlay").style.display = "block";
|
|
this.canvas.style.cursor = "crosshair";
|
|
|
|
var d = document.getElementById("ui-intel-brief");
|
|
if (d) d.innerText = "V.A.T.S. DEACTIVATED.";
|
|
},
|
|
|
|
|
|
start: function () {
|
|
if (TD.vats_charge < 100 || TD.vats_active) return;
|
|
|
|
TD.vats_active = true;
|
|
TD.vats_charge = 0;
|
|
TD.vats_shots = 6;
|
|
TD.vats_timer = 10 * TD.exp_fps; // 10 Seconds
|
|
|
|
// Slow time drastically
|
|
TD.global_speed_backup = TD.global_speed;
|
|
TD.global_speed = 0.001;
|
|
|
|
// Visuals
|
|
document.getElementById("vats-overlay").style.display = "block";
|
|
this.canvas.style.cursor = "crosshair";
|
|
|
|
var d = document.getElementById("ui-intel-brief");
|
|
if (d) d.innerText = "V.A.T.S. ENGAGED. SELECT TARGETS.";
|
|
},
|
|
|
|
endVats: function() {
|
|
if (!TD.vats_active) return;
|
|
|
|
TD.vats_active = false;
|
|
TD.vats_shots = 0;
|
|
TD.vats_timer = 0;
|
|
|
|
// Restore speed
|
|
if (TD.global_speed_backup) TD.global_speed = TD.global_speed_backup;
|
|
else TD.global_speed = 0.2; // Safe fallback
|
|
|
|
// Restore Visuals
|
|
document.getElementById("vats-overlay").style.display = "none";
|
|
this.canvas.style.cursor = "default";
|
|
|
|
var vatsBox = document.getElementById("vats-tooltip");
|
|
if (vatsBox) vatsBox.style.display = "none"; // FORCE HIDE ON END
|
|
|
|
var vatsBtn = document.getElementById("btn-vats-activate");
|
|
|
|
if (vatsBtn) {
|
|
vatsBtn.style.backgroundColor = ""; // Clear solid green/yellow
|
|
vatsBtn.style.boxShadow = ""; // Clear glow
|
|
vatsBtn.style.color = "#4caf50";
|
|
}
|
|
|
|
var d = document.getElementById("ui-intel-brief");
|
|
if (d) d.innerText = "V.A.T.S. DEACTIVATED.";
|
|
},
|
|
|
|
|
|
start: function () {
|
|
clearTimeout(this._st);
|
|
var _this = this;
|
|
this._exp_fps_0 = this.exp_fps - 0.4;
|
|
this._exp_fps_1 = this.exp_fps + 0.4;
|
|
this.mode = "normal";
|
|
this.eventManager.clear();
|
|
this.lang.mix(this, this.defaultSettings());
|
|
|
|
// --- NEW: S.P.E.C.I.A.L. INITIALIZATION ---
|
|
TD.perks = { S: false, P: false, E: false, C: false, I: false, A: false, L: false };
|
|
if (window.selectedSPECIALs) {
|
|
for (var i = 0; i < window.selectedSPECIALs.length; i++) {
|
|
TD.perks[window.selectedSPECIALs[i]] = true;
|
|
}
|
|
}
|
|
TD.overdrive = { S: 0, P: 0, E: 0, C: 0, I: 0, A: 0, L: 0 };
|
|
TD.overdrive_cd = { S: 0, P: 0, E: 0, C: 0, I: 0, A: 0, L: 0 };
|
|
|
|
this.stage = new TD.Stage("stage-main", TD.getDefaultStageData("stage_main"));
|
|
this.canvas.setAttribute("width", this.stage.width);
|
|
this.canvas.setAttribute("height", this.stage.height);
|
|
|
|
this.canvas.onmousemove = function (e) {
|
|
if (window.is_demo_mode) return;
|
|
var xy = _this.getEventXY.call(_this, e);
|
|
|
|
// Track global mouse position for ghost rendering
|
|
TD.mouse_x = xy[0];
|
|
TD.mouse_y = xy[1];
|
|
|
|
// --- NEW: V.A.T.S. Hover Interceptor ---
|
|
if (TD.vats_active) {
|
|
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
var hit_enemy = null;
|
|
var closest_dist = 999999;
|
|
var vatsBox = document.getElementById('vats-tooltip');
|
|
|
|
// Strict circular hit detection & closest sorting for stacked enemies
|
|
for (var i = 0; i < map.monsters.length; i++) {
|
|
var m = map.monsters[i];
|
|
if (m.is_valid) {
|
|
var dist = Math.sqrt(Math.pow(m.cx - xy[0], 2) + Math.pow(m.cy - xy[1], 2));
|
|
if (dist <= m.r) { // Must be physically touching the monster's circle
|
|
if (dist < closest_dist) {
|
|
closest_dist = dist;
|
|
hit_enemy = m;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hit_enemy && vatsBox) {
|
|
// Calculate damage and current HP percentage (Avg 300 DMG)
|
|
var damagePercent = Math.min(100, Math.floor((300 / Math.max(1, hit_enemy.life)) * 100));
|
|
var hpPercent = Math.max(0, Math.min(100, (hit_enemy.life / hit_enemy.life0) * 100));
|
|
|
|
// Inject into the new HTML elements
|
|
document.getElementById('vats-tooltip-hp').style.width = hpPercent + "%";
|
|
document.getElementById('vats-tooltip-perc').innerText = damagePercent + "%";
|
|
|
|
vatsBox.style.display = "block";
|
|
|
|
// Center the targeting box exactly on the mouse cursor
|
|
vatsBox.style.left = (e.clientX - 30) + "px"; // Half of 60px width
|
|
vatsBox.style.top = (e.clientY - 22) + "px"; // Half of 45px height
|
|
} else if (vatsBox) {
|
|
|
|
vatsBox.style.display = "none";
|
|
}
|
|
return; // Stop standard tooltips from rendering
|
|
}
|
|
|
|
// Normal Hover
|
|
_this.hover(xy[0], xy[1]);
|
|
|
|
// Allows for "click and drag" painting in Admin Mode
|
|
if (TD.admin_mode && _this.is_mouse_down) {
|
|
_this.applyAdminBrush(xy[0], xy[1]);
|
|
}
|
|
// --- NEW: Mine Placement Cursor & Tooltip Logic ---
|
|
if (TD.mode === "deploy_cryomine" || TD.mode === "deploy_capmine") {
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
if (map) {
|
|
var mx = Math.floor((xy[0] - map.x) / TD.grid_size);
|
|
var my = Math.floor((xy[1] - map.y) / TD.grid_size);
|
|
var grid = map.getGrid(mx, my);
|
|
|
|
var tooltip = document.getElementById('tower-tooltip');
|
|
|
|
// Valid path checking: MUST be passable (a path) and MUST NOT have a tower built on it
|
|
if (grid && grid.passable_flag == 1 && grid.build_flag == 0) {
|
|
TD.mouseHand(true); // Crosshair/Pointer
|
|
if (tooltip) tooltip.style.display = "none";
|
|
} else {
|
|
_this.canvas.style.cursor = "not-allowed"; // Red circle slash
|
|
if (tooltip) {
|
|
tooltip.innerText = "Must be placed on path.";
|
|
tooltip.style.display = "block";
|
|
tooltip.style.left = (e.pageX + 15) + "px";
|
|
tooltip.style.top = (e.pageY + 15) + "px";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
this.canvas.onmousedown = function (e) { _this.is_mouse_down = true; };
|
|
this.canvas.onmouseup = function (e) { _this.is_mouse_down = false; };
|
|
|
|
this.canvas.onclick = function (e) {
|
|
if (window.is_demo_mode) return;
|
|
var xy = _this.getEventXY.call(_this, e);
|
|
|
|
// --- NEW: V.A.T.S. Shooting Logic ---
|
|
if (TD.vats_active) {
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
var hit_enemy = null;
|
|
var closest_dist = 999999;
|
|
|
|
// Check if we clicked an enemy (strict hit detection & sorting)
|
|
for (var i = 0; i < map.monsters.length; i++) {
|
|
var m = map.monsters[i];
|
|
if (m.is_valid) {
|
|
var dist = Math.sqrt(Math.pow(m.cx - xy[0], 2) + Math.pow(m.cy - xy[1], 2));
|
|
if (dist <= m.r) {
|
|
if (dist < closest_dist) {
|
|
closest_dist = dist;
|
|
hit_enemy = m;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hit_enemy) {
|
|
|
|
TD.vats_shots--;
|
|
|
|
// Visual Blast (Laser impact style)
|
|
TD.Explode("vats-hit-" + TD.lang.rndStr(), { cx: hit_enemy.cx, cy: hit_enemy.cy, r: 15 * _TD.retina, step_level: 1, render_level: 9, color: "#00ff00", scene: map.scene, time: 0.3 });
|
|
|
|
// 200 to 400 Random True Damage (Bypasses armor/shields entirely)
|
|
var dmg = Math.floor(Math.random() * 201) + 200;
|
|
hit_enemy.life -= dmg;
|
|
|
|
TD.score += Math.floor(dmg * 0.1); // Score scales with damage dealt
|
|
|
|
// Pop damage text directly
|
|
new TD.FloatingText("dmg-" + TD.lang.rndStr(), {
|
|
cx: hit_enemy.cx + (Math.random() * 24 - 12),
|
|
cy: hit_enemy.cy - 10 + (Math.random() * 10 - 5),
|
|
text: "-" + dmg,
|
|
color: "#ffcc00",
|
|
map: hit_enemy.map,
|
|
size: 14,
|
|
life: 24
|
|
});
|
|
|
|
// Handle death directly if it kills them
|
|
if (hit_enemy.life <= 0) {
|
|
hit_enemy.beKilled({ killed: 0, gainXp: function(){} });
|
|
}
|
|
|
|
if (TD.vats_shots <= 0) TD.endVats();
|
|
}
|
|
return; // Block normal clicking while VATS is active
|
|
}
|
|
|
|
|
|
if (TD.admin_mode) {
|
|
_this.applyAdminBrush(xy[0], xy[1]);
|
|
} else if (TD.mode === "deploy_handy" && TD.stage && TD.stage.current_act) {
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
var handyData = TD.factionData[TD.current_faction] ? TD.factionData[TD.current_faction][10] : null;
|
|
var cost = (handyData && handyData.free_first_build) ? 0 : 500;
|
|
|
|
if (map && TD.money >= cost && !map.mr_handy_active) {
|
|
TD.money -= cost;
|
|
if (handyData && handyData.free_first_build) {
|
|
handyData.free_first_build = false;
|
|
TD.loadFaction(TD.current_faction);
|
|
}
|
|
map.mr_handy_active = true;
|
|
// Pass the exact mouse coordinates to Mr. Handy's init
|
|
|
|
new TD.MrHandy("handy-" + TD.lang.rndStr(), { map: map, cx: xy[0], cy: xy[1] });
|
|
TD.mode = "normal";
|
|
TD.mouseHand(false);
|
|
var d = document.getElementById("ui-action-desc");
|
|
if (d) d.innerText = "MR. HANDY DEPLOYED.";
|
|
}
|
|
} else if (TD.mode === "deploy_cryomine" && TD.stage && TD.stage.current_act) {
|
|
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
if (map) {
|
|
var mx = Math.floor((xy[0] - map.x) / TD.grid_size);
|
|
var my = Math.floor((xy[1] - map.y) / TD.grid_size);
|
|
var grid = map.getGrid(mx, my);
|
|
|
|
if (grid && grid.passable_flag == 1 && grid.build_flag == 0) { // <--- Added restriction
|
|
TD.inventory.cryomine--;
|
|
TD.cooldowns.cryomine = (TD.inventory.cryomine > 0) ? TD.getCooldownFrames() : 0;
|
|
TD.updateItemButton("cryomine");
|
|
|
|
new TD.CryoMine("cryo-" + TD.lang.rndStr(), { grid: grid, map: map });
|
|
|
|
TD.mode = "normal";
|
|
TD.mouseHand(false);
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "CRYO MINE PLANTED.";
|
|
} else {
|
|
// --- NEW: Undo selection on invalid click ---
|
|
TD.mode = "normal";
|
|
TD.mouseHand(false);
|
|
var tooltip = document.getElementById('tower-tooltip');
|
|
if (tooltip) tooltip.style.display = "none";
|
|
var d = document.getElementById("ui-intel-brief");
|
|
if(d) d.innerText = "MINE PLACEMENT CANCELLED.";
|
|
}
|
|
}
|
|
|
|
} else if (TD.mode === "deploy_capmine" && TD.stage && TD.stage.current_act) {
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
if (map) {
|
|
var mx = Math.floor((xy[0] - map.x) / TD.grid_size);
|
|
var my = Math.floor((xy[1] - map.y) / TD.grid_size);
|
|
var grid = map.getGrid(mx, my);
|
|
if (grid && grid.passable_flag == 1 && grid.build_flag == 0) { // <--- Added restriction
|
|
TD.inventory.capmine--;
|
|
TD.cooldowns.capmine = (TD.inventory.capmine > 0) ? TD.getCooldownFrames() : 0;
|
|
TD.updateItemButton("capmine");
|
|
new TD.CapMine("cap-" + TD.lang.rndStr(), { grid: grid, map: map });
|
|
TD.mode = "normal";
|
|
TD.mouseHand(false);
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "CAP MINE PLANTED.";
|
|
} else {
|
|
// --- NEW: Undo selection on invalid click ---
|
|
TD.mode = "normal";
|
|
TD.mouseHand(false);
|
|
var tooltip = document.getElementById('tower-tooltip');
|
|
if (tooltip) tooltip.style.display = "none";
|
|
var d = document.getElementById("ui-intel-brief");
|
|
if(d) d.innerText = "MINE PLACEMENT CANCELLED.";
|
|
}
|
|
}
|
|
} else if (TD.mode === "deploy_artillery" && TD.stage && TD.stage.current_act) {
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
if (map) {
|
|
var mx = Math.floor((xy[0] - map.x) / TD.grid_size);
|
|
var my = Math.floor((xy[1] - map.y) / TD.grid_size);
|
|
var grid = map.getGrid(mx, my);
|
|
if (grid && grid.passable_flag == 1 && grid.build_flag == 0) { // <--- Added restriction
|
|
TD.inventory.artillery--;
|
|
TD.cooldowns.artillery = (TD.inventory.artillery > 0) ? TD.getCooldownFrames() : 0;
|
|
TD.updateItemButton("artillery");
|
|
|
|
var blast_radius = TD.grid_size * 4;
|
|
TD.Explode("artillery-" + TD.lang.rndStr(), { cx: xy[0], cy: xy[1], r: blast_radius / _TD.retina, step_level: 1, render_level: 9, color: "#ff5500", scene: map.scene, time: 1.0 });
|
|
|
|
var dummy_source = { killed: 0, gainXp: function(){} };
|
|
TD.lang.each(map.monsters, function (m) {
|
|
if (m.is_valid && (Math.pow(m.cx - xy[0], 2) + Math.pow(m.cy - xy[1], 2) <= Math.pow(blast_radius, 2))) {
|
|
m.beHit(dummy_source, 300); // 300 Damage
|
|
}
|
|
});
|
|
TD.mode = "normal"; TD.mouseHand(false);
|
|
var d = document.getElementById("ui-action-desc"); if(d) d.innerText = "ARTILLERY STRIKE CONFIRMED.";
|
|
} else {
|
|
var d = document.getElementById("ui-action-desc"); if(d) d.innerText = "ERROR: MUST TARGET MONSTER PATH.";
|
|
}
|
|
}
|
|
} else if (TD.mode === "deploy_ncr_artillery" && TD.stage && TD.stage.current_act) {
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
if (map) {
|
|
var mx = Math.floor((xy[0] - map.x) / TD.grid_size);
|
|
var my = Math.floor((xy[1] - map.y) / TD.grid_size);
|
|
var startGrid = map.getGrid(mx, my);
|
|
|
|
if (startGrid && startGrid.passable_flag == 1 && startGrid.build_flag == 0) {
|
|
TD.inventory.ncr_artillery--;
|
|
TD.cooldowns.ncr_artillery = (TD.inventory.ncr_artillery > 0) ? TD.getCooldownFrames() : 0;
|
|
TD.updateItemButton("ncr_artillery");
|
|
|
|
// Drop targeting flare immediately
|
|
TD.Explode("flare-" + TD.lang.rndStr(), { cx: xy[0], cy: xy[1], r: 5 * _TD.retina, step_level: 1, render_level: 9, color: "#ff0000", scene: map.scene, time: 2.0 });
|
|
|
|
// Find path to exit to plan the walking barrage
|
|
var ext = map.exits[0];
|
|
var barragePath = [[mx, my]]; // Always bomb the clicked spot first
|
|
|
|
if (ext) {
|
|
var fw = new TD.FindWay(map.grid_x, map.grid_y, mx, my, ext.mx, ext.my, function (x, y) { return map.checkPassable(x, y); });
|
|
if (fw.is_arrived && fw.way.length > 0) {
|
|
var steps = Math.min(5, fw.way.length);
|
|
for (var i = 0; i < steps; i++) barragePath.push(fw.way[i]);
|
|
}
|
|
}
|
|
|
|
var dummy_source = { killed: 0, gainXp: function(){} };
|
|
var blast_radius = TD.grid_size * 2.5;
|
|
|
|
barragePath.forEach(function(coords, index) {
|
|
setTimeout(function() {
|
|
if (TD.is_paused) return;
|
|
var tcx = map.x + (coords[0] * TD.grid_size) + (TD.grid_size / 2);
|
|
var tcy = map.y + (coords[1] * TD.grid_size) + (TD.grid_size / 2);
|
|
|
|
TD.Explode("ncr-art-" + index, { cx: tcx, cy: tcy, r: blast_radius / _TD.retina, step_level: 1, render_level: 9, color: "#ff7700", scene: map.scene, time: 0.8 });
|
|
|
|
TD.lang.each(map.monsters, function (m) {
|
|
if (m.is_valid && (Math.pow(m.cx - tcx, 2) + Math.pow(m.cy - tcy, 2) <= Math.pow(blast_radius, 2))) {
|
|
m.beHit(dummy_source, 200);
|
|
}
|
|
});
|
|
}, index * 400);
|
|
});
|
|
|
|
TD.mode = "normal"; TD.mouseHand(false);
|
|
var d = document.getElementById("ui-action-desc"); if(d) d.innerText = "NCR BARRAGE COORDINATES LOCKED.";
|
|
} else {
|
|
var d = document.getElementById("ui-action-desc"); if(d) d.innerText = "ERROR: MUST TARGET MONSTER PATH.";
|
|
}
|
|
}
|
|
} else if (TD.mode === "deploy_bos_laser" && TD.stage && TD.stage.current_act) {
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
if (map) {
|
|
var mx = Math.floor((xy[0] - map.x) / TD.grid_size);
|
|
var my = Math.floor((xy[1] - map.y) / TD.grid_size);
|
|
var grid = map.getGrid(mx, my);
|
|
if (grid && grid.passable_flag == 1 && grid.build_flag == 0) {
|
|
TD.inventory.bos_laser--;
|
|
TD.cooldowns.bos_laser = TD.getCooldownFrames();
|
|
TD.updateItemButton("bos_laser");
|
|
new TD.OrbitalLaser("laser-" + TD.lang.rndStr(), { map: map, cx: xy[0], cy: xy[1] });
|
|
TD.mode = "normal"; TD.mouseHand(false);
|
|
var d = document.getElementById("ui-action-desc"); if(d) d.innerText = "ORBITAL STRIKE ACTIVE.";
|
|
} else {
|
|
var d = document.getElementById("ui-action-desc"); if(d) d.innerText = "ERROR: MUST TARGET MONSTER PATH.";
|
|
}
|
|
}
|
|
} else if (TD.mode === "deploy_mm_flare" && TD.stage && TD.stage.current_act) {
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
var mx = Math.floor((xy[0] - map.x) / TD.grid_size);
|
|
var my = Math.floor((xy[1] - map.y) / TD.grid_size);
|
|
var grid = map.getGrid(mx, my);
|
|
if (grid && grid.passable_flag == 1 && grid.build_flag == 0) {
|
|
TD.inventory.mm_flare--;
|
|
TD.cooldowns.mm_flare = TD.getCooldownFrames();
|
|
TD.updateItemButton("mm_flare");
|
|
new TD.MilitiaBlockade("militia-" + TD.lang.rndStr(), { map: map, cx: grid.cx, cy: grid.cy });
|
|
TD.mode = "normal"; TD.mouseHand(false);
|
|
var d = document.getElementById("ui-action-desc"); if(d) d.innerText = "MILITIA DEPLOYED.";
|
|
} else {
|
|
var d = document.getElementById("ui-action-desc"); if(d) d.innerText = "ERROR: MUST TARGET MONSTER PATH.";
|
|
}
|
|
} else if (TD.mode === "deploy_raider_nuka" && TD.stage && TD.stage.current_act) {
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
if (map) {
|
|
var mx = Math.floor((xy[0] - map.x) / TD.grid_size);
|
|
var my = Math.floor((xy[1] - map.y) / TD.grid_size);
|
|
var grid = map.getGrid(mx, my);
|
|
if (grid && grid.passable_flag == 1 && grid.build_flag == 0) {
|
|
TD.inventory.raider_nuka--;
|
|
TD.cooldowns.raider_nuka = TD.getCooldownFrames();
|
|
TD.updateItemButton("raider_nuka");
|
|
new TD.RadZone("nuka-" + TD.lang.rndStr(), { map: map, cx: xy[0], cy: xy[1] });
|
|
TD.mode = "normal"; TD.mouseHand(false);
|
|
var d = document.getElementById("ui-action-desc"); if(d) d.innerText = "NUKA GRENADE OUT!";
|
|
} else {
|
|
var d = document.getElementById("ui-action-desc"); if(d) d.innerText = "ERROR: MUST TARGET MONSTER PATH.";
|
|
}
|
|
}
|
|
} else {
|
|
_this.click(xy[0], xy[1]);
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
// New dedicated function to forcefully paint the grid
|
|
this.applyAdminBrush = function(px, py) {
|
|
if (!this.stage || !this.stage.current_act || !this.stage.current_act.current_scene || !this.stage.current_act.current_scene.map) return;
|
|
var map = this.stage.current_act.current_scene.map;
|
|
|
|
// Figure out which grid square was clicked based on pixel coordinates
|
|
var mx = Math.floor((px - map.x) / TD.grid_size);
|
|
var my = Math.floor((py - map.y) / TD.grid_size);
|
|
|
|
var grid = map.getGrid(mx, my);
|
|
if (!grid) return;
|
|
|
|
if (TD.admin_brush == "path") { grid.passable_flag = 1; grid.build_flag = 0; }
|
|
else if (TD.admin_brush == "block") { grid.passable_flag = 0; grid.build_flag = 0; }
|
|
else if (TD.admin_brush == "clear") {
|
|
// 1. Clear standard grid flags and Towers
|
|
grid.passable_flag = 1; grid.build_flag = 1;
|
|
if (grid.building) grid.removeBuilding();
|
|
|
|
// 2. Clear Entrances and Exits
|
|
if (grid.is_entrance) { grid.is_entrance = false; map.entrances = map.entrances.filter(function(e) { return e !== grid; }); map.entrance = map.entrances[0] || null; }
|
|
if (grid.is_exit) { grid.is_exit = false; map.exits = map.exits.filter(function(e) { return e !== grid; }); map.exit = map.exits[0] || null; }
|
|
|
|
// 3. Clear Assets (checks if your click pixel was inside any asset's bounding box)
|
|
if (map.assets) {
|
|
for (var i = map.assets.length - 1; i >= 0; i--) {
|
|
var a = map.assets[i];
|
|
if (px >= a.x && px <= a.x + a.width && py >= a.y && py <= a.y + a.height) {
|
|
// Reset the grid flags underneath the asset
|
|
for (var dx = 0; dx < a.grid_x; dx++) {
|
|
for (var dy = 0; dy < a.grid_y; dy++) {
|
|
var g = map.getGrid(a.mx + dx, a.my + dy);
|
|
if (g) { g.passable_flag = 1; g.build_flag = 1; }
|
|
}
|
|
}
|
|
// Delete the asset
|
|
map.assets.splice(i, 1);
|
|
break; // Only erase one asset per click
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
else if (TD.admin_brush == "entrance") {
|
|
if (grid.is_entrance) {
|
|
grid.dir = ((grid.dir || 0) + 1) % 4; // Rotate it
|
|
} else {
|
|
grid.is_entrance = true; grid.passable_flag = 1; grid.build_flag = 0; grid.dir = 0;
|
|
if (!map.entrances) map.entrances = [];
|
|
map.entrances.push(grid);
|
|
map.entrance = map.entrances[0]; // fallback
|
|
}
|
|
}
|
|
else if (TD.admin_brush == "exit") {
|
|
if (grid.is_exit) {
|
|
grid.dir = ((grid.dir || 0) + 1) % 4; // Rotate it
|
|
} else {
|
|
grid.is_exit = true; grid.passable_flag = 1; grid.build_flag = 0; grid.dir = 0;
|
|
if (!map.exits) map.exits = [];
|
|
map.exits.push(grid);
|
|
map.exit = map.exits[0]; // fallback
|
|
}
|
|
}
|
|
else if (TD.admin_brush == "tower") {
|
|
var towerType = document.getElementById("admin-tower-select").value;
|
|
var towerLvl = parseInt(document.getElementById("admin-tower-level").value) || 1;
|
|
|
|
grid.addBuilding(towerType);
|
|
grid.build_flag = 2; // Mark as built
|
|
grid.passable_flag = 0; // Towers block the path
|
|
|
|
// Apply upgrades instantly
|
|
if (grid.building) {
|
|
for (var i = 1; i < towerLvl; i++) {
|
|
grid.building.upgrade();
|
|
}
|
|
}
|
|
}
|
|
else if (TD.admin_brush == "asset" && TD.current_editor_asset) {
|
|
|
|
|
|
// NEW: Place the selected asset
|
|
if (!map.assets) map.assets = [];
|
|
var assetData = TD.current_editor_asset;
|
|
|
|
// Bottom-Anchoring: The mouse click (mx, my) is treated as the BOTTOM-LEFT tile of the asset.
|
|
var base_mx = mx;
|
|
var base_my = my - (assetData.grid_y - 1);
|
|
|
|
// Check if it fits within bounds
|
|
if (base_mx + assetData.grid_x > map.grid_x || base_my < 0 || base_mx < 0 || base_my + assetData.grid_y > map.grid_y) {
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "ERROR: Asset bounds off map.";
|
|
return;
|
|
}
|
|
|
|
var imgObj = new Image();
|
|
imgObj.src = assetData.src;
|
|
|
|
var x_px = map.x + (base_mx * TD.grid_size);
|
|
var y_px = map.y + (base_my * TD.grid_size);
|
|
var w_px = assetData.grid_x * TD.grid_size;
|
|
var h_px = assetData.grid_y * TD.grid_size;
|
|
|
|
var newAsset = {
|
|
id: assetData.id + "_" + Date.now() + "_" + Math.floor(Math.random() * 1000),
|
|
src: assetData.src,
|
|
img: imgObj,
|
|
mx: base_mx, my: base_my,
|
|
grid_x: assetData.grid_x, grid_y: assetData.grid_y,
|
|
blocks_path: assetData.blocks_path,
|
|
category: assetData.category, // CRITICAL FIX: Save the category so the renderer knows it's a path!
|
|
x: x_px, y: y_px, width: w_px, height: h_px,
|
|
y2: y_px + h_px,
|
|
is_asset: true
|
|
};
|
|
|
|
|
|
|
|
map.assets.push(newAsset);
|
|
|
|
for (var i = 0; i < assetData.grid_x; i++) {
|
|
for (var j = 0; j < assetData.grid_y; j++) {
|
|
var g = map.getGrid(base_mx + i, base_my + j);
|
|
if (g) {
|
|
// Visual paths don't block building on top of them!
|
|
if (assetData.category !== "path") g.build_flag = 0;
|
|
if (assetData.blocks_path) g.passable_flag = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
var d = document.getElementById("ui-action-desc");
|
|
if (assetData.category === "path") {
|
|
if (d) d.innerText = "Placed: " + assetData.name + " (Right-click to stop)";
|
|
} else {
|
|
// Deselect standard props after 1 placement
|
|
if (d) d.innerText = "Placed: " + assetData.name;
|
|
TD.current_editor_asset = null;
|
|
TD.admin_brush = "clear";
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
this.is_paused = false;
|
|
this.stage.start();
|
|
|
|
// --- FIX: Load the correct map data into the running scene ---
|
|
var lvl = window.selectedLevelName;
|
|
if (lvl && lvl !== "stage_main") {
|
|
var cfg = TD.getDefaultStageData(lvl);
|
|
if (cfg && cfg.map && this.stage && this.stage.current_act && this.stage.current_act.current_scene) {
|
|
this.stage.cfg = cfg; // Save the config for wave custom data
|
|
|
|
if (cfg.bg_image) {
|
|
var board = document.getElementById("td-board");
|
|
if (board) {
|
|
board.style.backgroundImage = "url('" + cfg.bg_image + "')";
|
|
board.style.backgroundSize = "100% 100%";
|
|
board.style.backgroundRepeat = "no-repeat";
|
|
}
|
|
}
|
|
|
|
// --- NEW: Load Game Environment Effect ---
|
|
var envOverlay = document.getElementById("env-effect-overlay");
|
|
if (envOverlay) {
|
|
if (cfg.map.env_effect) {
|
|
var eCfg = cfg.map.env_effect;
|
|
envOverlay.style.backgroundImage = "url('" + eCfg.src + "')";
|
|
document.documentElement.style.setProperty("--env-opacity", eCfg.opacity);
|
|
envOverlay.style.animation = "none";
|
|
void envOverlay.offsetWidth;
|
|
if (eCfg.type === "static") { envOverlay.style.opacity = eCfg.opacity; }
|
|
else if (eCfg.type === "fade") { envOverlay.style.opacity = 0; envOverlay.style.animation = "env-fade " + eCfg.speed + "s infinite ease-in-out"; }
|
|
else { envOverlay.style.opacity = eCfg.opacity; envOverlay.style.animation = "env-" + eCfg.type.replace("_", "-") + " " + eCfg.speed + "s infinite linear"; }
|
|
envOverlay.style.display = "block";
|
|
} else {
|
|
envOverlay.style.display = "none";
|
|
}
|
|
}
|
|
|
|
var m = this.stage.current_act.current_scene.map;
|
|
|
|
m.map_type = cfg.map.map_type || "traditional";
|
|
TD.lang.each(m.grids, function(g) { g.passable_flag = 1; g.build_flag = 1; g.is_entrance = false; g.is_exit = false; });
|
|
m.entrances = []; m.exits = []; m.entrance = null; m.exit = null;
|
|
m.assets = [];
|
|
|
|
if (cfg.map.grids_cfg) {
|
|
TD.lang.each(cfg.map.grids_cfg, function (obj) {
|
|
var grid = m.getGrid(obj.pos[0], obj.pos[1]);
|
|
if (grid) {
|
|
if (!isNaN(obj.passable_flag)) grid.passable_flag = obj.passable_flag;
|
|
if (!isNaN(obj.build_flag)) grid.build_flag = obj.build_flag;
|
|
if (obj.building) {
|
|
grid.addBuilding(obj.building);
|
|
if (obj.building_level && grid.building) {
|
|
for (var lvl2 = 1; lvl2 < obj.building_level; lvl2++) grid.building.upgrade();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (cfg.map.entrance) { var eg = m.getGrid(cfg.map.entrance[0], cfg.map.entrance[1]); if(eg) { eg.is_entrance = true; eg.passable_flag = 1; eg.build_flag = 0; m.entrances.push(eg); } }
|
|
if (cfg.map.entrances) {
|
|
for (var i = 0; i < cfg.map.entrances.length; i++) {
|
|
var eg = m.getGrid(cfg.map.entrances[i][0], cfg.map.entrances[i][1]);
|
|
if (eg) { eg.is_entrance = true; eg.passable_flag = 1; eg.build_flag = 0; m.entrances.push(eg); }
|
|
}
|
|
}
|
|
if (cfg.map.exit) { var xg = m.getGrid(cfg.map.exit[0], cfg.map.exit[1]); if(xg) { xg.is_exit = true; xg.passable_flag = 1; xg.build_flag = 0; m.exits.push(xg); } }
|
|
if (cfg.map.exits) {
|
|
for (var i = 0; i < cfg.map.exits.length; i++) {
|
|
var xg = m.getGrid(cfg.map.exits[i][0], cfg.map.exits[i][1]);
|
|
if (xg) { xg.is_exit = true; xg.passable_flag = 1; xg.build_flag = 0; m.exits.push(xg); }
|
|
}
|
|
}
|
|
m.entrance = m.entrances[0] || null;
|
|
m.exit = m.exits[0] || null;
|
|
|
|
if (cfg.map.assets) {
|
|
var _mapRef = m; // Crucial fix: Store reference to the map
|
|
TD.lang.each(cfg.map.assets, function (a) {
|
|
var imgObj = new Image();
|
|
imgObj.src = a.src;
|
|
var x_px = _mapRef.x + (a.mx * TD.grid_size);
|
|
var y_px = _mapRef.y + (a.my * TD.grid_size);
|
|
var w_px = a.grid_x * TD.grid_size;
|
|
var h_px = a.grid_y * TD.grid_size;
|
|
|
|
_mapRef.assets.push({
|
|
id: a.id, src: a.src, img: imgObj,
|
|
mx: a.mx, my: a.my, grid_x: a.grid_x, grid_y: a.grid_y,
|
|
blocks_path: a.blocks_path, category: a.category,
|
|
x: x_px, y: y_px, width: w_px, height: h_px,
|
|
y2: y_px + h_px, is_asset: true
|
|
});
|
|
for (var dx = 0; dx < a.grid_x; dx++) {
|
|
for (var dy = 0; dy < a.grid_y; dy++) {
|
|
var g = _mapRef.getGrid(a.mx + dx, a.my + dy);
|
|
if (g) {
|
|
if (a.category !== "path") g.build_flag = 0;
|
|
if (a.blocks_path) g.passable_flag = 0;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// ----------------------------------------------------------------------
|
|
|
|
this.step();
|
|
return this;
|
|
},
|
|
|
|
checkCheat: function (cheat_code) {
|
|
switch (cheat_code) {
|
|
case "money+": this.money += 1000000; break;
|
|
case "life+": this.life = 100; break;
|
|
case "life-": this.life = 1; break;
|
|
case "difficulty+": this.difficulty *= 2; break;
|
|
case "difficulty-": this.difficulty /= 2; break;
|
|
}
|
|
},
|
|
step: function () {
|
|
if (this.is_debug && _TD && _TD.cheat) {
|
|
this.checkCheat(_TD.cheat);
|
|
_TD.cheat = "";
|
|
}
|
|
|
|
if (this.is_paused) return; // Only stop entirely if the game is strictly paused
|
|
|
|
this.iframe++;
|
|
if (this.iframe % 50 == 0) {
|
|
var t = (new Date()).getTime(), step_time = this.step_time;
|
|
this.fps = Math.round(500000 / (t - this.last_iframe_time)) / 10;
|
|
this.last_iframe_time = t;
|
|
if (this.fps < this._exp_fps_0 && step_time > 1) { step_time--; }
|
|
else if (this.fps > this._exp_fps_1) { step_time++; }
|
|
this.step_time = step_time;
|
|
}
|
|
if (this.iframe % 2400 == 0) TD.gc();
|
|
|
|
if (this.admin_mode) {
|
|
// EDITOR OVERRIDE: Skip game math, but KEEP RENDERING so brushes update visually!
|
|
this.stage.render();
|
|
} else {
|
|
// NORMAL GAMEPLAY
|
|
this.stage.step();
|
|
this.stage.render();
|
|
}
|
|
|
|
var _this = this;
|
|
this._st = setTimeout(function () { _this.step(); }, this.step_time);
|
|
},
|
|
getEventXY: function (e) {
|
|
var rect = this.canvas.getBoundingClientRect();
|
|
// Dynamically calculate the exact scale between physical CSS size and internal Game size
|
|
var scaleX = this.canvas.width / rect.width;
|
|
var scaleY = this.canvas.height / rect.height;
|
|
|
|
var x = (e.clientX - rect.left) * scaleX;
|
|
var y = (e.clientY - rect.top) * scaleY;
|
|
|
|
// Prevent coordinates from bleeding off the edge
|
|
if (x < 0) x = 0;
|
|
if (y < 0) y = 0;
|
|
if (x > this.canvas.width) x = this.canvas.width;
|
|
if (y > this.canvas.height) y = this.canvas.height;
|
|
|
|
return [x, y];
|
|
},
|
|
|
|
|
|
|
|
|
|
hover: function (x, y) { this.eventManager.hover(x, y); },
|
|
click: function (x, y) { this.eventManager.click(x, y); },
|
|
mouseHand: function (v) { this.canvas.style.cursor = v ? "pointer" : "default"; },
|
|
log: function (txt) { this.is_debug && window.console && console.log && console.log(txt); },
|
|
gc: function () {
|
|
if (window.CollectGarbage) { CollectGarbage(); setTimeout(CollectGarbage, 1); }
|
|
}
|
|
};
|
|
for (i = 0; this.a[i]; i++) { this.a[i](TD); }
|
|
delete this.a;
|
|
TD.init(td_board);
|
|
}
|
|
};
|
|
|
|
_TD.a.push(function (TD) {
|
|
TD.lang = {
|
|
$e: function (el_id) { return document.getElementById(el_id); },
|
|
$c: function (tag_name, attributes, parent_node) {
|
|
var el = document.createElement(tag_name);
|
|
attributes = attributes || {};
|
|
for (var k in attributes) { if (attributes.hasOwnProperty(k)) { el.setAttribute(k, attributes[k]); } }
|
|
if (parent_node) parent_node.appendChild(el);
|
|
return el;
|
|
},
|
|
strLeft: function (s, n) {
|
|
var s2 = s.slice(0, n), i = s2.replace(/[^\x00-\xff]/g, "**").length;
|
|
if (i <= n) return s2;
|
|
i -= s2.length;
|
|
switch (i) {
|
|
case 0: return s2;
|
|
case n: return s.slice(0, n >> 1);
|
|
default:
|
|
var k = n - i, s3 = s.slice(k, n), j = s3.replace(/[\x00-\xff]/g, "").length;
|
|
return j ? s.slice(0, k) + this.arguments.callee(s3, j) : s.slice(0, k);
|
|
}
|
|
},
|
|
strLen2: function (s) { return s.replace(/[^\x00-\xff]/g, "**").length; },
|
|
each: function (list, f) {
|
|
if (Array.prototype.forEach) { list.forEach(f); }
|
|
else { for (var i = 0, l = list.length; i < l; i++) { f(list[i]); } }
|
|
},
|
|
any: function (list, f) {
|
|
for (var i = 0, l = list.length; i < l; i++) { if (f(list[i])) return list[i]; }
|
|
return null;
|
|
},
|
|
shift: function (list, f) { while (list[0]) { f(list.shift()); } },
|
|
rndSort: function (list) {
|
|
var a = list.concat();
|
|
return a.sort(function () { return Math.random() - 0.5; });
|
|
},
|
|
_rndRGB2: function (v) { var s = v.toString(16); return s.length == 2 ? s : ("0" + s); },
|
|
rndRGB: function () {
|
|
var r = Math.floor(Math.random() * 256), g = Math.floor(Math.random() * 256), b = Math.floor(Math.random() * 256);
|
|
return "#" + this._rndRGB2(r) + this._rndRGB2(g) + this._rndRGB2(b);
|
|
},
|
|
rgb2Arr: function (rgb_str) {
|
|
if (rgb_str.length != 7) return [0, 0, 0];
|
|
var r = rgb_str.substr(1, 2), g = rgb_str.substr(3, 2), b = rgb_str.substr(3, 2);
|
|
return [parseInt(r, 16), parseInt(g, 16), parseInt(b, 16)];
|
|
},
|
|
rndStr: function (n) {
|
|
n = n || 16;
|
|
var chars = "1234567890abcdefghijklmnopqrstuvwxyz", a = [], i, chars_len = chars.length, r;
|
|
for (i = 0; i < n; i++) {
|
|
r = Math.floor(Math.random() * chars_len);
|
|
a.push(chars.substr(r, 1));
|
|
}
|
|
return a.join("");
|
|
},
|
|
nullFunc: function () {},
|
|
arrayEqual: function (arr1, arr2) {
|
|
var i, l = arr1.length;
|
|
if (l != arr2.length) return false;
|
|
for (i = 0; i < l; i++) { if (arr1[i] != arr2[i]) return false; }
|
|
return true;
|
|
},
|
|
mix: function (r, s, is_overwrite) {
|
|
if (!s || !r) return r;
|
|
for (var p in s) {
|
|
if (s.hasOwnProperty(p) && (is_overwrite !== false || !(p in r))) { r[p] = s[p]; }
|
|
}
|
|
return r;
|
|
}
|
|
};
|
|
});
|
|
|
|
_TD.a.push(function (TD) {
|
|
TD.eventManager = {
|
|
ex: -1, ey: -1, _registers: {}, ontypes: [ "enter", "hover", "out", "click" ], current_type: "hover",
|
|
isOn: function (el) {
|
|
return (this.ex != -1 && this.ey != -1 && this.ex > el.x && this.ex < el.x2 && this.ey > el.y && this.ey < el.y2);
|
|
},
|
|
_mkElEvtName: function (el, evt_type) { return el.id + "::_evt_::" + evt_type; },
|
|
on: function (el, evt_type, f) { this._registers[this._mkElEvtName(el, evt_type)] = [el, evt_type, f]; },
|
|
removeEventListener: function (el, evt_type) {
|
|
var en = this._mkElEvtName(el, evt_type);
|
|
delete this._registers[en];
|
|
},
|
|
clear: function () { delete this._registers; this._registers = {}; },
|
|
step: function () {
|
|
if (!this.current_type) return;
|
|
var k, a, el, et, f, j, _this = this, ontypes_len = this.ontypes.length, is_evt_on, to_del_el = [];
|
|
for (k in this._registers) {
|
|
if (!this._registers.hasOwnProperty(k)) continue;
|
|
a = this._registers[k]; el = a[0]; et = a[1]; f = a[2];
|
|
if (!el.is_valid) { to_del_el.push(el); continue; }
|
|
if (!el.is_visiable) continue;
|
|
is_evt_on = this.isOn(el);
|
|
if (this.current_type != "click") {
|
|
if (et == "hover" && el.is_hover && is_evt_on) { f(); this.current_type = "hover"; }
|
|
else if (et == "enter" && !el.is_hover && is_evt_on) { el.is_hover = true; f(); this.current_type = "enter"; }
|
|
else if (et == "out" && el.is_hover && !is_evt_on) { el.is_hover = false; f(); this.current_type = "out"; }
|
|
} else {
|
|
if (is_evt_on && et == "click") f();
|
|
}
|
|
}
|
|
TD.lang.each(to_del_el, function (obj) {
|
|
for (j = 0; j < ontypes_len; j++) _this.removeEventListener(obj, _this.ontypes[j]);
|
|
});
|
|
this.current_type = "";
|
|
},
|
|
hover: function (ex, ey) {
|
|
if (this.current_type == "click") return;
|
|
this.current_type = "hover"; this.ex = ex; this.ey = ey;
|
|
},
|
|
click: function (ex, ey) { this.current_type = "click"; this.ex = ex; this.ey = ey; }
|
|
};
|
|
});
|
|
|
|
_TD.a.push(function (TD) {
|
|
TD.Stage = function (id, cfg) {
|
|
this.id = id || ("stage-" + TD.lang.rndStr());
|
|
this.cfg = cfg || {};
|
|
this.width = this.cfg.width || 640;
|
|
this.height = this.cfg.height || 540;
|
|
this.mode = "normal";
|
|
this.state = 0;
|
|
this.acts = [];
|
|
this.current_act = null;
|
|
this._step2 = TD.lang.nullFunc;
|
|
this._init();
|
|
};
|
|
TD.Stage.prototype = {
|
|
_init: function () {
|
|
if (typeof this.cfg.init == "function") { this.cfg.init.call(this); }
|
|
if (typeof this.cfg.step2 == "function") { this._step2 = this.cfg.step2; }
|
|
},
|
|
start: function () {
|
|
this.state = 1;
|
|
TD.lang.each(this.acts, function (obj) { obj.start(); });
|
|
},
|
|
pause: function () { this.state = 2; },
|
|
gameover: function () { this.current_act.gameover(); },
|
|
clear: function () {
|
|
this.state = 3;
|
|
TD.lang.each(this.acts, function (obj) { obj.clear(); });
|
|
},
|
|
step: function () {
|
|
if (this.state != 1 || !this.current_act) return;
|
|
TD.eventManager.step();
|
|
this.current_act.step();
|
|
this._step2();
|
|
},
|
|
render: function () {
|
|
if (this.state == 0 || this.state == 3 || !this.current_act) return;
|
|
this.current_act.render();
|
|
},
|
|
addAct: function (act) { this.acts.push(act); },
|
|
addElement: function (el, step_level, render_level) {
|
|
if (this.current_act) this.current_act.addElement(el, step_level, render_level);
|
|
}
|
|
};
|
|
TD.Act = function (stage, id) {
|
|
this.stage = stage;
|
|
this.id = id || ("act-" + TD.lang.rndStr());
|
|
this.state = 0;
|
|
this.scenes = [];
|
|
this.end_queue = [];
|
|
this.current_scene = null;
|
|
this._init();
|
|
};
|
|
TD.Act.prototype = {
|
|
_init: function () { this.stage.addAct(this); },
|
|
start: function () {
|
|
if (this.stage.current_act && this.stage.current_act.state != 3) {
|
|
this.state = 0;
|
|
this.stage.current_act.queue(this.start);
|
|
return;
|
|
}
|
|
this.state = 1;
|
|
this.stage.current_act = this;
|
|
TD.lang.each(this.scenes, function (obj) { obj.start(); });
|
|
},
|
|
pause: function () { this.state = 2; },
|
|
end: function () {
|
|
this.state = 3;
|
|
var f;
|
|
while (f = this.end_queue.shift()) { f(); }
|
|
this.stage.current_act = null;
|
|
},
|
|
queue: function (f) { this.end_queue.push(f); },
|
|
clear: function () {
|
|
this.state = 3;
|
|
TD.lang.each(this.scenes, function (obj) { obj.clear(); });
|
|
},
|
|
step: function () {
|
|
if (this.state != 1 || !this.current_scene) return;
|
|
this.current_scene.step();
|
|
},
|
|
render: function () {
|
|
if (this.state == 0 || this.state == 3 || !this.current_scene) return;
|
|
this.current_scene.render();
|
|
},
|
|
addScene: function (scene) { this.scenes.push(scene); },
|
|
addElement: function (el, step_level, render_level) {
|
|
if (this.current_scene) this.current_scene.addElement(el, step_level, render_level);
|
|
},
|
|
gameover: function () { this.current_scene.gameover(); }
|
|
};
|
|
TD.Scene = function (act, id) {
|
|
this.act = act;
|
|
this.stage = act.stage;
|
|
this.is_gameover = false;
|
|
this.id = id || ("scene-" + TD.lang.rndStr());
|
|
this.state = 0;
|
|
this.end_queue = [];
|
|
this._step_elements = [ [], [], [] ];
|
|
this._render_elements = [ [], [], [], [], [], [], [], [], [], [] ];
|
|
this._init();
|
|
};
|
|
TD.Scene.prototype = {
|
|
_init: function () { this.act.addScene(this); this.wave = 0; },
|
|
start: function () {
|
|
if (this.act.current_scene && this.act.current_scene != this && this.act.current_scene.state != 3) {
|
|
this.state = 0;
|
|
this.act.current_scene.queue(this.start);
|
|
return;
|
|
}
|
|
this.state = 1;
|
|
this.act.current_scene = this;
|
|
},
|
|
pause: function () { this.state = 2; },
|
|
end: function () {
|
|
this.state = 3;
|
|
var f;
|
|
while (f = this.end_queue.shift()) { f(); }
|
|
this.clear();
|
|
this.act.current_scene = null;
|
|
},
|
|
clear: function () {
|
|
TD.lang.shift(this._step_elements, function (obj) {
|
|
TD.lang.shift(obj, function (obj2) { obj2.del(); });
|
|
});
|
|
TD.lang.shift(this._render_elements, function (obj) {
|
|
TD.lang.shift(obj, function (obj2) { obj2.del(); });
|
|
});
|
|
},
|
|
queue: function (f) { this.end_queue.push(f); },
|
|
gameover: function () {
|
|
if (this.is_gameover) return;
|
|
this.pause();
|
|
this.is_gameover = true;
|
|
},
|
|
step: function () {
|
|
if (this.state != 1) return;
|
|
if (TD.life <= 0) { TD.life = 0; this.gameover(); }
|
|
var i, a;
|
|
for (i = 0; i < 3; i++) {
|
|
a = [];
|
|
var level_elements = this._step_elements[i];
|
|
TD.lang.shift(level_elements, function (obj) {
|
|
if (obj.is_valid) {
|
|
if (!obj.is_paused) obj.step();
|
|
a.push(obj);
|
|
} else { setTimeout(function () { obj = null; }, 500); }
|
|
});
|
|
this._step_elements[i] = a;
|
|
}
|
|
},
|
|
render: function () {
|
|
if (this.state == 0 || this.state == 3) return;
|
|
var i, a, ctx = TD.ctx;
|
|
ctx.clearRect(0, 0, this.stage.width, this.stage.height);
|
|
for (i = 0; i < 10; i++) {
|
|
a = [];
|
|
var level_elements = this._render_elements[i];
|
|
TD.lang.shift(level_elements, function (obj) {
|
|
if (obj.is_valid) {
|
|
if (obj.is_visiable) obj.render();
|
|
a.push(obj);
|
|
}
|
|
});
|
|
this._render_elements[i] = a;
|
|
}
|
|
if (this.is_gameover) { this.panel.gameover_obj.show(); }
|
|
},
|
|
addElement: function (el, step_level, render_level) {
|
|
step_level = step_level || el.step_level || 1;
|
|
render_level = render_level || el.render_level;
|
|
this._step_elements[step_level].push(el);
|
|
this._render_elements[render_level].push(el);
|
|
el.scene = this;
|
|
el.step_level = step_level;
|
|
el.render_level = render_level;
|
|
}
|
|
};
|
|
});
|
|
|
|
_TD.a.push(function (TD) {
|
|
TD.Element = function (id, cfg) {
|
|
this.id = id || ("el-" + TD.lang.rndStr());
|
|
this.cfg = cfg || {};
|
|
this.is_valid = true;
|
|
this.is_visiable = typeof cfg.is_visiable != "undefined" ? cfg.is_visiable : true;
|
|
this.is_paused = false;
|
|
this.is_hover = false;
|
|
this.x = this.cfg.x || -1;
|
|
this.y = this.cfg.y || -1;
|
|
this.width = this.cfg.width || 0;
|
|
this.height = this.cfg.height || 0;
|
|
this.step_level = cfg.step_level || 1;
|
|
this.render_level = cfg.render_level;
|
|
this.on_events = cfg.on_events || [];
|
|
this._init();
|
|
};
|
|
TD.Element.prototype = {
|
|
_init: function () {
|
|
var _this = this, i, en, len;
|
|
for (i = 0, len = this.on_events.length; i < len; i++) {
|
|
en = this.on_events[i];
|
|
switch (en) {
|
|
case "enter": this.on("enter", function () { _this.onEnter(); }); break;
|
|
case "out": this.on("out", function () { _this.onOut(); }); break;
|
|
case "hover": this.on("hover", function () { _this.onHover(); }); break;
|
|
case "click": this.on("click", function () { _this.onClick(); }); break;
|
|
}
|
|
}
|
|
this.caculatePos();
|
|
},
|
|
caculatePos: function () {
|
|
this.cx = this.x + this.width / 2;
|
|
this.cy = this.y + this.height / 2;
|
|
this.x2 = this.x + this.width;
|
|
this.y2 = this.y + this.height;
|
|
},
|
|
start: function () { this.is_paused = false; },
|
|
pause: function () { this.is_paused = true; },
|
|
hide: function () { this.is_visiable = false; this.onOut(); },
|
|
show: function () { this.is_visiable = true; },
|
|
del: function () { this.is_valid = false; },
|
|
on: function (evt_type, f) { TD.eventManager.on(this, evt_type, f); },
|
|
onEnter: TD.lang.nullFunc, onOut: TD.lang.nullFunc, onHover: TD.lang.nullFunc, onClick: TD.lang.nullFunc, step: TD.lang.nullFunc, render: TD.lang.nullFunc,
|
|
addToScene: function (scene, step_level, render_level, pre_add_list) {
|
|
this.scene = scene;
|
|
if (isNaN(step_level)) return;
|
|
this.step_level = step_level || this.step_level;
|
|
this.render_level = render_level || this.render_level;
|
|
if (pre_add_list) {
|
|
TD.lang.each(pre_add_list, function (obj) { scene.addElement(obj, step_level, render_level); });
|
|
}
|
|
scene.addElement(this, step_level, render_level);
|
|
}
|
|
};
|
|
});
|
|
|
|
_TD.a.push(function (TD) {
|
|
TD._msg_texts = {
|
|
"_cant_build": "Can't build here!", "_cant_pass": "Can't pass!", "entrance": "Entrance", "exit": "Exit",
|
|
"not_enough_money": "Not enough money, need $${0}.", "wave_info": "Wave ${0}", "panel_money_title": "Money: ",
|
|
"panel_score_title": "Score: ", "panel_life_title": "Life: ", "panel_building_title": "Buildings: ", "panel_monster_title": "Monsters: ",
|
|
"building_name_wall": "Roadblock", "building_name_cannon": "Cannon", "building_name_LMG": "LMG", "building_name_HMG": "HMG",
|
|
"building_name_laser_gun": "Laser gun", "building_info": "${0}: Level ${1}, Damage ${2}, Speed ${3}, Range ${4}, Kill ${5}",
|
|
"building_info_wall": "${0}", "building_intro_wall": "Roadblock: monsters could not pass ($${0})",
|
|
"building_intro_cannon": "Cannon: balance in range and damage ($${0})", "building_intro_LMG": "Light Machine Gun: longer range, normal damage ($${0})",
|
|
"building_intro_HMG": "Heavy Machine Gun: fast shoot, greater damage, normal range ($${0})", "building_intro_laser_gun": "Laser gun: greater damage, 100% hit ($${0})",
|
|
"click_to_build": "Left click to build ${0} ($${1})", "upgrade": "Upgrade ${0} to level ${1} , cost $${2}。",
|
|
"sell": "Sell ${0} for $${1}", "upgrade_success": "Upgrade success! ${0} upgrades to level ${1}. Next upgrade will take $${2}.",
|
|
"monster_info": "Monster: Life ${0}, Shield ${1}, Speed ${2}, Damage ${3}", "button_build_text": "Build", "button_upgrade_text": "Upgrade",
|
|
"button_sell_text": "Sell", "button_start_text": "Start", "button_restart_text": "Restart", "button_pause_text": "Pause",
|
|
"button_continue_text": "Continue", "button_pause_desc_0": "Pause the game", "button_pause_desc_1": "Resume the game",
|
|
"blocked": "Can't build here, it will block the way from entrance to exit!", "monster_be_blocked": "Can't build here, some monster will be blocked!",
|
|
"entrance_or_exit_be_blocked": "Can't build on the entrance or the exit!", "_": "ERROR"
|
|
};
|
|
TD._t = TD.translate = function (k, args) {
|
|
args = (typeof args == "object" && args.constructor == Array) ? args : [];
|
|
var msg = this._msg_texts[k] || this._msg_texts["_"], i, l = args.length;
|
|
for (i = 0; i < l; i++) { msg = msg.replace("${" + i + "}", args[i]); }
|
|
return msg;
|
|
};
|
|
});
|
|
|
|
_TD.a.push(function (TD) {
|
|
TD.default_upgrade_rule = function (old_level, old_value) { return old_value * 1.2; };
|
|
|
|
// --- NEW: DYNAMIC TACTICAL MATCHUP GENERATOR ---
|
|
TD.getTacticalMatchups = function(tower) {
|
|
var strong = [], weak = [], cannot = [];
|
|
var vType = tower.visual_type || tower.type;
|
|
var spd = tower.speed || 1.0;
|
|
|
|
var isEarlyTower = (tower.type === "tower_1" || tower.type === "tower_2" || tower.type === "tower_3" || (tower.id && (tower.id.endsWith("_1") || tower.id.endsWith("_2") || tower.id.endsWith("_3"))));
|
|
|
|
if (isEarlyTower) {
|
|
cannot.push("Air");
|
|
} else if (vType === "laser_gun" || vType === "LMG" || vType === "HMG" || vType === "rifle") {
|
|
strong.push("Air");
|
|
} else {
|
|
weak.push("Air");
|
|
}
|
|
|
|
if (spd >= 3.0) { strong.push("Swarms"); weak.push("Armor"); }
|
|
else if (spd <= 1.0) { strong.push("Armor"); weak.push("Swarms"); }
|
|
|
|
if (vType === "cannon") strong.push("Groups");
|
|
if (tower.is_cryo) strong.push("Fast Foes");
|
|
if (tower.is_detector) { strong = ["Stealth"]; weak = ["Combat"]; }
|
|
|
|
if (strong.length === 0) strong.push("General");
|
|
if (weak.length === 0) weak.push("None");
|
|
|
|
return { strong: strong.join(", "), weak: weak.join(", "), cannot: cannot.join(", ") };
|
|
};
|
|
|
|
|
|
TD.getDefaultBuildingAttributes = function (building_type) {
|
|
var faction = null;
|
|
var idx = -1;
|
|
|
|
if (building_type && building_type.indexOf("tower_") === 0 && TD.current_faction) {
|
|
idx = parseInt(building_type.split("_")[1]) - 1;
|
|
faction = TD.current_faction;
|
|
} else if (building_type) {
|
|
// Check specific faction IDs
|
|
if (building_type === "mr_handy") { faction = TD.current_faction || "NCR"; idx = 10; }
|
|
else if (building_type.indexOf("ncr_") === 0) { faction = "NCR"; idx = parseInt(building_type.split("_")[1]) - 1; }
|
|
else if (building_type.indexOf("bos_") === 0) { faction = "BOS"; idx = parseInt(building_type.split("_")[1]) - 1; }
|
|
else if (building_type.indexOf("mm_") === 0) { faction = "MINUTEMEN"; idx = parseInt(building_type.split("_")[1]) - 1; }
|
|
else if (building_type.indexOf("rk_") === 0) { faction = "RAIDERS"; idx = parseInt(building_type.split("_")[1]) - 1; }
|
|
}
|
|
|
|
if (faction && idx >= 0 && TD.factionData[faction] && TD.factionData[faction][idx]) {
|
|
var tData = TD.factionData[faction][idx];
|
|
var attrs = {};
|
|
|
|
// Clone stats directly from Faction Data
|
|
for (var k in tData) attrs[k] = tData[k];
|
|
|
|
// CRITICAL FIX: Ensure the engine uses the internal type name, NOT the visual fallback type
|
|
attrs.type = building_type;
|
|
|
|
// Default upgrade rules
|
|
attrs._upgrade_rule_damage = function(old_level, old_value) { return old_value * 1.25; };
|
|
attrs._upgrade_rule_range = function(old_level, old_value) { return old_value + 0.1; };
|
|
attrs._upgrade_rule_speed = function(old_level, old_value) { return old_value * 1.05; };
|
|
|
|
return attrs;
|
|
}
|
|
|
|
// If a tower fails to load or is invalid, return a harmless, non-firing block
|
|
TD.log("ERROR: Attempted to load invalid tower type: " + building_type);
|
|
return { damage: 0, range: 0, speed: 0, bullet_speed: 0, life: 100, shield: 100, cost: 9999, type: "error" };
|
|
};
|
|
|
|
|
|
|
|
// --- NEW: FACTION TOWER DATA ---
|
|
|
|
TD.factionData = {
|
|
"NCR": typeof ncr_towers !== 'undefined' ? ncr_towers : [],
|
|
"BOS": typeof bos_towers !== 'undefined' ? bos_towers : [],
|
|
"MINUTEMEN": typeof mm_towers !== 'undefined' ? mm_towers : [],
|
|
"RAIDERS": typeof raider_towers !== 'undefined' ? raider_towers : []
|
|
};
|
|
|
|
// Inject Mr. Handy as the 11th "tower" for all factions
|
|
for (var f in TD.factionData) {
|
|
if (TD.factionData.hasOwnProperty(f)) {
|
|
TD.factionData[f].push({
|
|
id: "mr_handy",
|
|
name: "MR. HANDY",
|
|
cost: 500,
|
|
unlock_cost: 4000,
|
|
is_unlocked: false,
|
|
damage: "Melee",
|
|
range: "Map",
|
|
speed: "Fast"
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// --- NEW: ASSET LIBRARY DATA ---
|
|
|
|
TD.assetLibrary = {
|
|
"Paths": [
|
|
{ id: "path_dirt", name: "Dirt Tile", src: "assets/images/path1.png", grid_x: 1, grid_y: 1, blocks_path: false, category: "path" },
|
|
{ id: "path_stone", name: "Stone Tile", src: "assets/images/path2.png", grid_x: 1, grid_y: 1, blocks_path: false, category: "path" }
|
|
],
|
|
|
|
"Wasteland General": [
|
|
{ id: "waste_1x1", name: "Small Box", src: "assets/images/0.png", grid_x: 1, grid_y: 1, blocks_path: true, category: "prop" },
|
|
{ id: "waste_1x2", name: "Tall Pillar", src: "assets/images/00.png", grid_x: 1, grid_y: 2, blocks_path: true, category: "prop" },
|
|
{ id: "waste_2x2", name: "Large Wreck", src: "assets/images/0000.png", grid_x: 2, grid_y: 2, blocks_path: true, category: "prop" }
|
|
],
|
|
|
|
"Nuka-World": [
|
|
// Example of how you can add more themes later
|
|
]
|
|
};
|
|
|
|
TD.current_editor_asset = null; // Stores the currently selected asset from the library
|
|
|
|
|
|
|
|
|
|
TD.loadFaction = function(factionName) {
|
|
TD.current_faction = factionName;
|
|
var towers = TD.factionData[factionName];
|
|
if (!towers) return;
|
|
|
|
for (let i = 0; i < 11; i++) {
|
|
let btn = document.getElementById("btn-buy-tower-" + (i + 1));
|
|
if (!btn) continue;
|
|
|
|
let tData = towers[i];
|
|
if (tData) {
|
|
// Notice: We removed the old campaign hardcoded limits!
|
|
// The Wave Editor handles tower visibility now.
|
|
|
|
// Dynamically assign unlock cost: Towers 5-11 (index 4-10) cost 1000 + 500 per tier
|
|
tData.unlock_cost = (i >= 4) ? (1000 + ((i - 4) * 500)) : 0;
|
|
|
|
if (tData.is_unlocked) {
|
|
var displayCost = tData.free_first_build ? "<span style='color:#0f0;'>(READY)</span>" : "(" + tData.cost + "c)";
|
|
btn.innerHTML = tData.name + " " + displayCost;
|
|
|
|
// If it was previously unlit, it just unlocked! Trigger the flicker.
|
|
|
|
if (btn.classList.contains('unlit')) {
|
|
btn.classList.remove('unlit');
|
|
btn.classList.remove('vats-charged-glow'); // Remove the flashing glow
|
|
btn.className = btn.className.replace(/warm-up-\d/g, "").trim();
|
|
void btn.offsetWidth; // Force CSS reflow
|
|
btn.classList.add("warm-up-" + (Math.floor(Math.random() * 5) + 1));
|
|
}
|
|
|
|
// Closure fix: capture the correct reference to tData
|
|
(function(td) {
|
|
btn.onmouseover = function() {
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (tooltip) {
|
|
var tact = TD.getTacticalMatchups(td);
|
|
var cannotHtml = tact.cannot ? "<br><span style='color:#ff5555'>Cannot target: " + tact.cannot + "</span>" : "";
|
|
tooltip.innerHTML = td.name + "<br>Dmg: " + td.damage + " | Rng: " + td.range + " | Spd: " + td.speed + "<br><br><span style='color:#0f0'>Strong vs: " + tact.strong + "</span><br><span style='color:#f55'>Weak vs: " + tact.weak + "</span>" + cannotHtml;
|
|
tooltip.style.display = "block";
|
|
}
|
|
};
|
|
})(tData);
|
|
|
|
|
|
} else {
|
|
var svgLock = "<svg width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' style='vertical-align:middle;margin-right:3px;'><rect x='3' y='11' width='18' height='11' rx='2' ry='2'></rect><path d='M7 11V7a5 5 0 0 1 10 0v4'></path></svg>";
|
|
|
|
// Locked but available to purchase
|
|
btn.innerHTML = svgLock + tData.name;
|
|
btn.classList.add('unlit');
|
|
|
|
// Closure fix: capture the correct reference to tData
|
|
(function(td) {
|
|
btn.onmouseover = function() {
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (tooltip) {
|
|
var tact = TD.getTacticalMatchups(td);
|
|
var cannotHtml = tact.cannot ? "<br><span style='color:#ff5555'>Cannot target: " + tact.cannot + "</span>" : "";
|
|
tooltip.innerHTML = td.name + " <span style='color:#ff5555'>[LOCKED]</span><br>Dmg: " + td.damage + " | Rng: " + td.range + " | Spd: " + td.speed + "<br><br><span style='color:#0f0'>Strong vs: " + tact.strong + "</span><br><span style='color:#f55'>Weak vs: " + tact.weak + "</span>" + cannotHtml + "<br><br>Unlock Cost: <span style='color:#ffaa00;font-weight:bold;'>" + td.unlock_cost + "c</span>";
|
|
tooltip.style.display = "block";
|
|
}
|
|
};
|
|
})(tData);
|
|
|
|
}
|
|
|
|
btn.onmouseout = function() {
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (tooltip) tooltip.style.display = "none";
|
|
};
|
|
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// --- NEW: DYNAMIC FACTION CONSUMABLE (6TH SLOT) ---
|
|
var btnSpecial = document.getElementById("btn-item-blank") || document.getElementById("btn-item-ncr_artillery") || document.getElementById("btn-item-bos_laser") || document.getElementById("btn-item-mm_flare") || document.getElementById("btn-item-raider_nuka");
|
|
|
|
if (btnSpecial) {
|
|
var specialData = {
|
|
"NCR": { id: "ncr_artillery", name: "BARRAGE", desc: "Target path for walking artillery sequence." },
|
|
"BOS": { id: "bos_laser", name: "ORBITAL", desc: "Guide a laser from the sky." },
|
|
"MINUTEMEN": { id: "mm_flare", name: "MILITIA", desc: "Summon blockaders on the path." },
|
|
"RAIDERS": { id: "raider_nuka", name: "NUKA GRENADE", desc: "Huge blast + radiation zone." }
|
|
};
|
|
var sData = specialData[factionName];
|
|
if (sData) {
|
|
// Morphs the HTML button into the correct item
|
|
btnSpecial.id = "btn-item-" + sData.id;
|
|
btnSpecial.title = sData.desc;
|
|
|
|
// Wire up click event to deploy
|
|
btnSpecial.onclick = function() {
|
|
if (TD.inventory[sData.id] > 0 && TD.cooldowns[sData.id] <= 0 && TD.stage && TD.stage.current_act) {
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
if (map) {
|
|
map.cancelPreBuild();
|
|
TD.mode = "deploy_" + sData.id;
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "DEPLOYING " + sData.name + ". Click map to target.";
|
|
TD.mouseHand(true);
|
|
}
|
|
}
|
|
};
|
|
TD.updateItemButton(sData.id); // Force visual update
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
|
|
_TD.a.push(function (TD) {
|
|
function defaultMonsterRender() {
|
|
if (!this.is_valid || !this.grid) return;
|
|
|
|
// NEW: Check if the calling context is the Y-sort queue, otherwise skip to prevent double-drawing
|
|
if (arguments[0] !== "ysort") return;
|
|
|
|
var ctx = TD.ctx;
|
|
|
|
// V.A.T.S. Highlight
|
|
if (TD.vats_active) {
|
|
ctx.strokeStyle = "#00ff00";
|
|
ctx.lineWidth = 3 * _TD.retina;
|
|
} else {
|
|
ctx.strokeStyle = "#000";
|
|
ctx.lineWidth = 1;
|
|
}
|
|
|
|
// Smooth Fading Render for Stealth
|
|
var rgb = TD.lang.rgb2Arr(this.color);
|
|
var alpha = typeof this.stealth_alpha !== 'undefined' ? this.stealth_alpha : 1.0;
|
|
|
|
if (this.is_stealthed || this.is_burrowing) {
|
|
// Draw faint hollow outline permanently while flagged as stealth/burrow
|
|
ctx.strokeStyle = "rgba(0, 255, 0, 0.3)";
|
|
ctx.lineWidth = 2 * _TD.retina;
|
|
}
|
|
|
|
// Draw the solid core fading in and out based on alpha
|
|
ctx.fillStyle = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + alpha + ")";
|
|
|
|
ctx.beginPath(); ctx.arc(this.cx, this.cy, this.r, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
|
|
// Only show health if they are visible enough
|
|
if (TD.show_monster_life && alpha > 0.3) {
|
|
|
|
var s = Math.floor(TD.grid_size / 4), l = s * 2 - 2 * _TD.retina;
|
|
ctx.fillStyle = "#000"; ctx.beginPath(); ctx.fillRect(this.cx - s, this.cy - this.r - 6, s * 2, 4 * _TD.retina); ctx.closePath();
|
|
ctx.fillStyle = "#f00"; ctx.beginPath(); ctx.fillRect(this.cx - s + _TD.retina, this.cy - this.r - (6 - _TD.retina), this.life * l / this.life0, 2 * _TD.retina); ctx.closePath();
|
|
}
|
|
}
|
|
TD.getDefaultMonsterAttributes = function (monster_idx) {
|
|
var monster_attributes = [
|
|
{ name: "Radroach", desc: "Weak, fast-ish pest", speed: 3, max_speed: 10, life: 50, damage: 1, shield: 0, money: 5 },
|
|
{ name: "Feral Ghoul", desc: "Mindless irradiated human", speed: 6, max_speed: 20, life: 50, damage: 2, shield: 1 },
|
|
{ name: "Mole Rat", desc: "Fast underground creature", speed: 12, max_speed: 30, life: 50, damage: 3, shield: 1 },
|
|
{ name: "Super Mutant", desc: "Large, mutated brute", speed: 5, max_speed: 10, life: 500, damage: 3, shield: 1 },
|
|
{ name: "Protectron", desc: "Armored security bot", speed: 5, max_speed: 10, life: 50, damage: 3, shield: 20 },
|
|
{ name: "Glowing One", desc: "Highly irradiated, deals massive base damage", speed: 7, max_speed: 14, life: 50, damage: 10, shield: 2 },
|
|
{ name: "Deathclaw", desc: "Extremely fast, tough apex predator", speed: 15, max_speed: 30, life: 100, damage: 3, shield: 3 },
|
|
{ name: "Sentry Bot", desc: "Heavily armored military robot", speed: 3, max_speed: 10, life: 300, damage: 5, shield: 15 },
|
|
|
|
// --- NEW: PHASED MONSTERS ---
|
|
{ name: "Cazador", is_flying: true, desc: "Lightning fast flying wasp", speed: 15, max_speed: 25, life: 40, damage: 4, shield: 1, color: "#dd9900" },
|
|
{ name: "Stingwing", is_flying: true, desc: "Agile flying pest", speed: 12, max_speed: 20, life: 25, damage: 2, shield: 0, color: "#88cc33" },
|
|
{ name: "Scorchbeast", is_flying: true, desc: "Massive flying terror", speed: 4, max_speed: 10, life: 2500, damage: 30, shield: 5, color: "#551111" },
|
|
{ name: "Nightkin", is_stealthed: true, desc: "Tough, stealthed mutant", speed: 8, max_speed: 15, life: 800, damage: 8, shield: 2, color: "#330066" }
|
|
];
|
|
|
|
if (typeof monster_idx == "undefined") { return monster_attributes.length; }
|
|
var attr = monster_attributes[monster_idx] || monster_attributes[0], attr2 = {};
|
|
TD.lang.mix(attr2, attr);
|
|
if (!attr2.render) { attr2.render = defaultMonsterRender; }
|
|
return attr2;
|
|
};
|
|
|
|
TD.WaveDirector = {
|
|
consecutive_breather_waves: 0,
|
|
survival_waves: [
|
|
[], // Index 0 (unused)
|
|
[[3, 0]], // Wave 1: 3 Roaches
|
|
[[5, 0]], // Wave 2: 5 Roaches
|
|
[[4, 0], [1, 1]], // Wave 3: 4 Roaches, 1 Ghoul
|
|
[[4, 0], [3, 1]], // Wave 4: 4 Roaches, 3 Ghouls
|
|
[[1, 0], [5, 1]], // Wave 5: 1 Roach, 5 Ghouls
|
|
[[2, 0], [2, 1], [1, 3]], // Wave 6: 2 Roaches, 2 Ghouls, 1 Mutant
|
|
[[3, 0], [3, 1], [2, 3]], // Wave 7: 3 Roaches, 3 Ghouls, 2 Mutants
|
|
[[2, 1], [3, 3]], // Wave 8: 2 Ghouls, 3 Mutants
|
|
[[4, 3]], // Wave 9: 4 Mutants
|
|
[[10, 1]] // Wave 10: 10 Ghouls (The Swarm)
|
|
],
|
|
|
|
generateDynamicWave: function(wave_idx, is_campaign, map) {
|
|
var difficulty = TD.difficulty || 1.0;
|
|
|
|
// 1. CALCULATE PLAYER DPS OUTPUT
|
|
var total_dps = 0;
|
|
|
|
if (map && map.buildings) {
|
|
TD.lang.each(map.buildings, function(b) {
|
|
if (b.is_weapon) {
|
|
var aps = b.speed > 0 ? (1 / b.speed) : 1;
|
|
var pierce_mult = b.pierce_count ? b.pierce_count : 1;
|
|
var aoe_mult = (b.visual_type === "cannon" || b.fire_pattern === "cluster") ? 3 : 1;
|
|
var dps = (b.damage * aps * pierce_mult * aoe_mult);
|
|
total_dps += dps;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 2. DYNAMIC BUDGETING
|
|
// Smoothly curve the base budget using wave count, and heavily nerf the DPS tax (from 70% to 15%)
|
|
var baseline_threat = (total_dps * 0.15) + (wave_idx * 3) + 15;
|
|
var budget = baseline_threat * difficulty;
|
|
|
|
var minimum_budget = wave_idx * 4;
|
|
var maximum_budget = 30 + (wave_idx * 10);
|
|
|
|
if (budget < minimum_budget) budget = minimum_budget;
|
|
if (budget > maximum_budget) budget = maximum_budget; // STOPS INFINITE INFLATION
|
|
|
|
|
|
// 3. THE BREATHER CHECK
|
|
|
|
var is_siege = map && map.map_type === "siege";
|
|
var hp_percent = is_siege ? (TD.base_health / 10) : TD.life;
|
|
var force_breather = false;
|
|
|
|
if (hp_percent < 25) {
|
|
if (this.consecutive_breather_waves === 0) {
|
|
budget *= 0.6; // 40% reduction for breather
|
|
force_breather = true;
|
|
this.consecutive_breather_waves = 1;
|
|
} else {
|
|
this.consecutive_breather_waves = 0; // No mercy
|
|
}
|
|
} else {
|
|
this.consecutive_breather_waves = 0; // Reset if healthy
|
|
}
|
|
|
|
// Bestiary: [Monster ID, Cost, Type, Max Per Wave, Batch Limit]
|
|
// Types: 0=Swarm, 1=Tank, 2=Flyer, 3=Stealth, 4=Boss
|
|
var bestiary = [
|
|
[0, 2, 0, 99, 5], [1, 3, 0, 99, 5], [2, 4, 0, 99, 5], // Swarms (Cheap, unlimited, max 5)
|
|
[4, 8, 1, 99, 3], [3, 12, 1, 99, 3], [5, 15, 1, 99, 2], [7, 25, 1, 99, 2], // Tanks (Slightly restricted)
|
|
[9, 5, 2, 99, 3], [8, 6, 2, 99, 3], // Flyers (Max 3 per cluster)
|
|
[11, 20, 3, 5, 1], // Stealth (Max 5 total, 1 at a time)
|
|
[6, 40, 4, 1, 1], [10, 60, 4, 1, 1] // Bosses (Max 1 total, 1 at a time)
|
|
];
|
|
|
|
// 4. TELEGRAPHED SCHEDULE & RESTRICTIONS
|
|
var allowed = bestiary.filter(function(m) {
|
|
if (m[2] === 4 && (wave_idx < 18 || force_breather)) return false;
|
|
if (m[2] === 3 && (wave_idx < 14 || force_breather)) return false;
|
|
if (m[2] === 2 && (wave_idx < 11)) return false;
|
|
return true;
|
|
});
|
|
|
|
// 5. ASSEMBLE WAVE
|
|
var wave_composition = [];
|
|
var spawn_counts = { 3: 0, 4: 0 };
|
|
|
|
// --- NEW: STRICT UNIT CAP TO PREVENT BLOBS ---
|
|
// HORDE MODE: Gentle growth until Wave 40, then explosive swarm growth
|
|
var max_units_this_wave = 10 + Math.floor(wave_idx * 0.5);
|
|
if (wave_idx > 40) {
|
|
max_units_this_wave += (wave_idx - 40) * 2; // AI swarm limit unleashes
|
|
}
|
|
var total_spawned = 0;
|
|
|
|
|
|
while (budget > 0 && allowed.length > 0 && total_spawned < max_units_this_wave) {
|
|
var pick = allowed[Math.floor(Math.random() * allowed.length)];
|
|
var id = pick[0], cost = pick[1], type = pick[2], max_wave = pick[3], max_batch = pick[4];
|
|
|
|
if (budget >= cost) {
|
|
var qty = Math.min(Math.floor(budget / cost), Math.floor(Math.random() * max_batch) + 1);
|
|
|
|
// Respect the max units physical limit
|
|
if (total_spawned + qty > max_units_this_wave) {
|
|
qty = max_units_this_wave - total_spawned;
|
|
}
|
|
|
|
if (type === 3 || type === 4) {
|
|
if (spawn_counts[type] + qty > max_wave) qty = max_wave - spawn_counts[type];
|
|
spawn_counts[type] += qty;
|
|
if (spawn_counts[type] >= max_wave) allowed = allowed.filter(function(m) { return m[2] !== type; });
|
|
}
|
|
|
|
if (qty > 0) {
|
|
wave_composition.push([qty, id]);
|
|
budget -= (cost * qty);
|
|
total_spawned += qty;
|
|
} else {
|
|
// Failsafe: Remove from pool to prevent infinite loops if qty gets clamped to 0
|
|
allowed = allowed.filter(function(m) { return m[2] !== type; });
|
|
}
|
|
} else {
|
|
allowed = allowed.filter(function(m) { return m[1] <= budget; });
|
|
}
|
|
}
|
|
return wave_composition;
|
|
|
|
}
|
|
};
|
|
TD.makeMonsters = function (n, range) {
|
|
var current_wave = 1;
|
|
var is_campaign = window.is_campaign_mode === true;
|
|
var map = null;
|
|
|
|
// Attempt to get real-time state
|
|
if (TD.stage && TD.stage.current_act && TD.stage.current_act.current_scene) {
|
|
current_wave = TD.stage.current_act.current_scene.wave + 1; // +1 for the incoming wave
|
|
map = TD.stage.current_act.current_scene.map;
|
|
}
|
|
|
|
// --- NEW: Custom Spawns Hook ---
|
|
if (TD.stage && TD.stage.cfg && TD.stage.cfg.custom_waves) {
|
|
var cWave = TD.stage.cfg.custom_waves[current_wave];
|
|
if (cWave && cWave.spawns && cWave.spawns.length > 0) {
|
|
var customArr = [];
|
|
for (var i = 0; i < cWave.spawns.length; i++) {
|
|
customArr.push([cWave.spawns[i].qty, cWave.spawns[i].monster_idx]);
|
|
}
|
|
return customArr;
|
|
}
|
|
}
|
|
// -------------------------------
|
|
|
|
// 1. Survival Predefined (Waves 1-10)
|
|
|
|
if (!is_campaign && current_wave <= 10) {
|
|
if (TD.WaveDirector.survival_waves[current_wave]) return TD.WaveDirector.survival_waves[current_wave];
|
|
}
|
|
|
|
// 2. Dynamic Curated Director (Survival 11+ OR Campaign 6+)
|
|
if ((!is_campaign && current_wave > 10) || (is_campaign && current_wave > 5)) {
|
|
if (map) return TD.WaveDirector.generateDynamicWave(current_wave, is_campaign, map);
|
|
}
|
|
|
|
// 3. Fallback for Initialization or Campaign 1-5 (if not hardcoded in maps.js)
|
|
n = n || 10;
|
|
var a = [], count = 0, i, c, d, r;
|
|
var max_allowed_idx = 7;
|
|
if (current_wave >= 12) max_allowed_idx = 11;
|
|
else if (current_wave >= 8) max_allowed_idx = 11;
|
|
else if (current_wave >= 6) max_allowed_idx = 9;
|
|
|
|
if (!range) { range = []; for (i = 0; i <= max_allowed_idx; i++) { range.push(i); } }
|
|
while (count < n) {
|
|
d = n - count;
|
|
c = Math.min(Math.floor(Math.random() * d) + 1, 3);
|
|
r = Math.floor(Math.random() * range.length);
|
|
a.push([c, range[r]]);
|
|
count += c;
|
|
}
|
|
return a;
|
|
};
|
|
|
|
});
|
|
|
|
_TD.a.push(function (TD) {
|
|
TD.FindWay = function (w, h, x1, y1, x2, y2, f_passable) {
|
|
this.m = []; this.w = w; this.h = h; this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2;
|
|
this.way = []; this.len = this.w * this.h; this.is_blocked = this.is_arrived = false;
|
|
this.fPassable = typeof f_passable == "function" ? f_passable : function () { return true; };
|
|
this._init();
|
|
};
|
|
TD.FindWay.prototype = {
|
|
_init: function () {
|
|
if (this.x1 == this.x2 && this.y1 == this.y2) { this.is_arrived = true; this.way = [[this.x1, this.y1]]; return; }
|
|
for (var i = 0; i < this.len; i++) this.m[i] = -2;
|
|
this.x = this.x1; this.y = this.y1; this.distance = 0; this.current = [[this.x, this.y]];
|
|
this.setVal(this.x, this.y, 0);
|
|
while (this.next()) {}
|
|
},
|
|
getVal: function (x, y) { var p = y * this.w + x; return p < this.len ? this.m[p] : -1; },
|
|
setVal: function (x, y, v) { var p = y * this.w + x; if (p > this.len) return false; this.m[p] = v; },
|
|
getNeighborsOf: function (x, y) {
|
|
var nbs = [];
|
|
if (y > 0) nbs.push([x, y - 1]);
|
|
if (x < this.w - 1) nbs.push([x + 1, y]);
|
|
if (y < this.h - 1) nbs.push([x, y + 1]);
|
|
if (x > 0) nbs.push([x - 1, y]);
|
|
return nbs;
|
|
},
|
|
getAllNeighbors: function () {
|
|
var nbs = [], nb1, i, c, l = this.current.length;
|
|
for (i = 0; i < l; i++) { c = this.current[i]; nb1 = this.getNeighborsOf(c[0], c[1]); nbs = nbs.concat(nb1); }
|
|
return nbs;
|
|
},
|
|
findWay: function () {
|
|
var x = this.x2, y = this.y2, nb, max_len = this.len, nbs_len, nbs, i, l, v, min_v = -1, closest_nbs;
|
|
while ((x != this.x1 || y != this.y1) && min_v != 0 && this.way.length < max_len) {
|
|
this.way.unshift([x, y]); nbs = this.getNeighborsOf(x, y); nbs_len = nbs.length; closest_nbs = []; min_v = -1;
|
|
for (i = 0; i < nbs_len; i++) { v = this.getVal(nbs[i][0], nbs[i][1]); if (v < 0) continue; if (min_v < 0 || min_v > v) min_v = v; }
|
|
for (i = 0; i < nbs_len; i++) { nb = nbs[i]; if (min_v == this.getVal(nb[0], nb[1])) { closest_nbs.push(nb); } }
|
|
l = closest_nbs.length; i = l > 1 ? Math.floor(Math.random() * l) : 0; nb = closest_nbs[i]; x = nb[0]; y = nb[1];
|
|
}
|
|
},
|
|
arrive: function () { this.current = []; this.is_arrived = true; this.findWay(); },
|
|
blocked: function () { this.current = []; this.is_blocked = true; },
|
|
next: function () {
|
|
var neighbors = this.getAllNeighbors(), nb, l = neighbors.length, valid_neighbors = [], x, y, i, v;
|
|
this.distance++;
|
|
for (i = 0; i < l; i++) {
|
|
nb = neighbors[i]; x = nb[0]; y = nb[1];
|
|
if (this.getVal(x, y) != -2) continue;
|
|
if (this.fPassable(x, y)) { v = this.distance; valid_neighbors.push(nb); }
|
|
else { v = -1; }
|
|
this.setVal(x, y, v);
|
|
if (x == this.x2 && y == this.y2) { this.arrive(); return false; }
|
|
}
|
|
if (valid_neighbors.length == 0) { this.blocked(); return false; }
|
|
this.current = valid_neighbors; return true;
|
|
}
|
|
};
|
|
});
|
|
|
|
_TD.a.push(function (TD) {
|
|
var grid_obj = {
|
|
_init: function (cfg) {
|
|
cfg = cfg || {}; this.map = cfg.map; this.scene = this.map.scene;
|
|
this.mx = cfg.mx; this.my = cfg.my; this.width = TD.grid_size; this.height = TD.grid_size;
|
|
this.is_entrance = this.is_exit = false; this.passable_flag = 1; this.build_flag = 1; this.building = null;
|
|
this.caculatePos();
|
|
},
|
|
caculatePos: function () {
|
|
this.x = this.map.x + this.mx * TD.grid_size; this.y = this.map.y + this.my * TD.grid_size;
|
|
this.x2 = this.x + TD.grid_size; this.y2 = this.y + TD.grid_size;
|
|
this.cx = Math.floor(this.x + TD.grid_size / 2); this.cy = Math.floor(this.y + TD.grid_size / 2);
|
|
},
|
|
checkBlock: function () {
|
|
if (this.is_entrance || this.is_exit) { this._block_msg = TD._t("entrance_or_exit_be_blocked"); return true; }
|
|
if (this.map.map_type === "siege") return false;
|
|
|
|
var is_blocked = false;
|
|
var _this = this;
|
|
|
|
// Every entrance must be able to reach at least one exit
|
|
if (this.map.entrances && this.map.exits) {
|
|
for (var i = 0; i < this.map.entrances.length; i++) {
|
|
var ent = this.map.entrances[i];
|
|
var canReach = false;
|
|
for (var j = 0; j < this.map.exits.length; j++) {
|
|
var ext = this.map.exits[j];
|
|
var fw = new TD.FindWay(this.map.grid_x, this.map.grid_y, ent.mx, ent.my, ext.mx, ext.my, function (x, y) { return !(x == _this.mx && y == _this.my) && _this.map.checkPassable(x, y); });
|
|
if (!fw.is_blocked) { canReach = true; break; }
|
|
}
|
|
if (!canReach) { is_blocked = true; break; }
|
|
}
|
|
}
|
|
|
|
if (!is_blocked) { is_blocked = !!this.map.anyMonster(function (obj) { return obj.chkIfBlocked(_this.mx, _this.my); }); if (is_blocked) this._block_msg = TD._t("monster_be_blocked"); }
|
|
else { this._block_msg = TD._t("blocked"); }
|
|
return is_blocked;
|
|
},
|
|
|
|
|
|
buyBuilding: function (building_type) {
|
|
var cost = TD.getDefaultBuildingAttributes(building_type).cost || 0;
|
|
var tData = null;
|
|
if (building_type && building_type.indexOf("tower_") === 0 && TD.current_faction) {
|
|
var idx = parseInt(building_type.split("_")[1]) - 1;
|
|
tData = TD.factionData[TD.current_faction][idx];
|
|
if (tData && tData.free_first_build) cost = 0;
|
|
}
|
|
|
|
if (TD.money >= cost) {
|
|
TD.money -= cost;
|
|
this.addBuilding(building_type);
|
|
// Consume the free build flag and update UI
|
|
if (tData && tData.free_first_build) {
|
|
tData.free_first_build = false;
|
|
TD.loadFaction(TD.current_faction);
|
|
}
|
|
}
|
|
else { TD.log(TD._t("not_enough_money", [cost])); this.scene.panel.balloontip.msg(TD._t("not_enough_money", [cost]), this); }
|
|
},
|
|
|
|
addBuilding: function (building_type) {
|
|
if (this.building) { this.removeBuilding(); }
|
|
var building = new TD.Building("building-" + building_type + "-" + TD.lang.rndStr(), { type: building_type, step_level: this.step_level, render_level: this.render_level });
|
|
building.locate(this); this.scene.addElement(building, this.step_level, this.render_level + 1);
|
|
this.map.buildings.push(building); this.building = building; this.build_flag = 2;
|
|
this.map.checkHasWeapon();
|
|
if (this.map.pre_building) this.map.pre_building.hide();
|
|
},
|
|
removeBuilding: function () { if (this.build_flag == 2) this.build_flag = 1; if (this.building) this.building.remove(); this.building = null; },
|
|
addMonster: function (monster) { monster.beAddToGrid(this); this.map.monsters.push(monster); monster.start(); },
|
|
hightLight: function (show) { this.map.select_hl[show ? "show" : "hide"](this); },
|
|
render: function () {
|
|
var ctx = TD.ctx, px = this.x + 0.5, py = this.y + 0.5;
|
|
if (this.is_hover && TD.mode === "build") { ctx.fillStyle = "rgba(255, 255, 200, 0.2)"; ctx.beginPath(); ctx.fillRect(px, py, this.width, this.height); ctx.closePath(); ctx.fill(); }
|
|
|
|
// ADMIN OVERLAY COLORS
|
|
|
|
if ((TD.admin_mode && TD.admin_overlay !== false) || TD.show_grid) {
|
|
// Path Brush: Monsters walk, no towers (Blue)
|
|
if (this.passable_flag == 1 && this.build_flag == 0) { ctx.fillStyle = "rgba(0, 100, 255, 0.3)"; ctx.beginPath(); ctx.fillRect(px, py, this.width, this.height); ctx.closePath(); ctx.fill(); }
|
|
// Block Brush: No monsters, no towers (Red)
|
|
else if (this.passable_flag == 0 && this.build_flag == 0) { ctx.fillStyle = "rgba(255, 0, 0, 0.3)"; ctx.beginPath(); ctx.fillRect(px, py, this.width, this.height); ctx.closePath(); ctx.fill(); }
|
|
|
|
// Note: The Default "Clear" state is passable=1 and build=1. We draw NOTHING here so you can see the background image.
|
|
}
|
|
|
|
|
|
// Only draw arrows if we are alive
|
|
var isAlive = (this.map.map_type === "siege") ? (TD.base_health > 0) : (TD.life > 0);
|
|
if ((this.is_entrance || this.is_exit) && !window.is_demo_mode && isAlive) {
|
|
// Sine wave using the global frame counter for a pulsing effect (0 to 1)
|
|
var pulseAlpha = Math.abs(Math.sin(TD.iframe / 12));
|
|
|
|
ctx.globalAlpha = 0.5 + (pulseAlpha * 0.5); // Minimum alpha adjusted so the outline remains visible
|
|
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.font = "bold " + Math.floor(TD.grid_size * 0.8) + "px Arial"; // Slightly larger arrow
|
|
ctx.lineWidth = 3 * _TD.retina; // Outline thickness
|
|
ctx.strokeStyle = "#000000"; // Crisp black outline
|
|
|
|
// Save context state to rotate just the arrow
|
|
ctx.save();
|
|
ctx.translate(this.cx, this.cy);
|
|
|
|
if (this.is_entrance) {
|
|
ctx.fillStyle = "#00ff00"; // Green
|
|
ctx.rotate((this.dir || 0) * Math.PI / 2);
|
|
ctx.strokeText("➤", 0, 0); // Draw black outline
|
|
ctx.fillText("➤", 0, 0); // Draw green arrow
|
|
} else {
|
|
ctx.fillStyle = "#ff0000"; // Red
|
|
ctx.rotate((this.dir || 0) * Math.PI / 2);
|
|
ctx.strokeText("➤", 0, 0); // Draw black outline
|
|
ctx.fillText("➤", 0, 0); // Draw red arrow
|
|
}
|
|
|
|
|
|
// Restore context state
|
|
ctx.restore();
|
|
|
|
ctx.globalAlpha = 1.0; // Reset global alpha so the rest of the game doesn't flash!
|
|
}
|
|
|
|
if (TD.show_grid) {
|
|
ctx.strokeStyle = "rgba(255, 255, 255, 0.15)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.strokeRect(px, py, this.width, this.height); ctx.closePath(); ctx.stroke();
|
|
}
|
|
},
|
|
|
|
onEnter: function () {
|
|
if (this.map.is_main_map && TD.mode == "build") {
|
|
if (this.build_flag == 1 || this.build_flag == 2) { this.map.pre_building.show(); this.map.pre_building.locate(this); }
|
|
else { this.map.pre_building.hide(); }
|
|
}
|
|
// Removed the annoying default hover balloon tips!
|
|
},
|
|
|
|
onOut: function () { if (this.scene.panel.balloontip.el == this) { this.scene.panel.balloontip.hide(); } },
|
|
onClick: function () {
|
|
if (TD.admin_mode) return; // Prevent normal clicks from firing while in admin mode
|
|
|
|
if (this.scene.state != 1) return;
|
|
|
|
if (TD.mode == "build" && this.map.is_main_map && !this.building) {
|
|
if (this.checkBlock()) { this.scene.panel.balloontip.msg(this._block_msg, this); }
|
|
else { this.buyBuilding(this.map.pre_building.type); }
|
|
} else if (!this.building && this.map.selected_building) {
|
|
this.map.selected_building.toggleSelected(); this.map.selected_building = null;
|
|
}
|
|
}
|
|
|
|
};
|
|
TD.Grid = function (id, cfg) {
|
|
cfg.on_events = ["enter", "out", "click"];
|
|
var grid = new TD.Element(id, cfg);
|
|
TD.lang.mix(grid, grid_obj); grid._init(cfg); return grid;
|
|
};
|
|
});
|
|
|
|
_TD.a.push(function (TD) {
|
|
var map_obj = {
|
|
_init: function (cfg) {
|
|
cfg = cfg || {}; this.grid_x = cfg.grid_x || 10; this.grid_y = cfg.grid_y || 10;
|
|
this.x = cfg.x || 0; this.y = cfg.y || 0; this.width = this.grid_x * TD.grid_size; this.height = this.grid_y * TD.grid_size;
|
|
this.x2 = this.x + this.width; this.y2 = this.y + this.height;
|
|
this.grids = []; this.entrances = []; this.exits = []; this.buildings = []; this.monsters = []; this.bullets = [];
|
|
this.scene = cfg.scene; this.is_main_map = !!cfg.is_main_map;
|
|
this.map_type = cfg.map_type || "traditional"; // NEW: Map Type tracking
|
|
this.spawn_mode = cfg.spawn_mode || "random"; // NEW: Spawn mode
|
|
this.select_hl = TD.MapSelectHighLight(this.id + "-hl", { map: this }); this.select_hl.addToScene(this.scene, 1, 9);
|
|
|
|
this.selected_building = null; this._wait_clearInvalidElements = 20; this._wait_add_monsters = 0; this._wait_add_monsters_arr = [];
|
|
var i, l = this.grid_x * this.grid_y, grid_data = cfg["grid_data"] || [], d, grid;
|
|
for (i = 0; i < l; i++) {
|
|
d = grid_data[i] || {}; d.mx = i % this.grid_x; d.my = Math.floor(i / this.grid_x); d.map = this; d.step_level = this.step_level; d.render_level = this.render_level;
|
|
grid = new TD.Grid(this.id + "-grid-" + d.mx + "-" + d.my, d); this.grids.push(grid);
|
|
}
|
|
|
|
// Load Entrances
|
|
if (cfg.entrance) { var eg = this.getGrid(cfg.entrance[0], cfg.entrance[1]); if (eg) { eg.is_entrance = true; this.entrances.push(eg); } }
|
|
if (cfg.entrances) {
|
|
for (var i = 0; i < cfg.entrances.length; i++) {
|
|
var eg = this.getGrid(cfg.entrances[i][0], cfg.entrances[i][1]);
|
|
if (eg) { eg.is_entrance = true; this.entrances.push(eg); }
|
|
}
|
|
}
|
|
// Load Exits
|
|
if (cfg.exit) { var xg = this.getGrid(cfg.exit[0], cfg.exit[1]); if (xg) { xg.is_exit = true; this.exits.push(xg); } }
|
|
if (cfg.exits) {
|
|
for (var i = 0; i < cfg.exits.length; i++) {
|
|
var xg = this.getGrid(cfg.exits[i][0], cfg.exits[i][1]);
|
|
if (xg) { xg.is_exit = true; this.exits.push(xg); }
|
|
}
|
|
}
|
|
|
|
// Fallbacks for legacy single-references in the engine
|
|
this.entrance = this.entrances[0] || null;
|
|
this.exit = this.exits[0] || null;
|
|
|
|
// --- DEMO MODE INITIAL BURST ---
|
|
if (window.is_demo_mode && this.entrances.length > 0) {
|
|
// Queue up 50 quick spawns instantly for absolute chaos
|
|
this._wait_add_monsters_arr = [];
|
|
for (var j = 0; j < 10; j++) {
|
|
this._wait_add_monsters_arr.push([5, Math.floor(Math.random() * TD.monster_type_count)]);
|
|
}
|
|
}
|
|
|
|
// --- NEW: DYNAMIC ASSET REBUILDER ---
|
|
this.assets = [];
|
|
if (cfg.assets) {
|
|
var _mapRef = this;
|
|
TD.lang.each(cfg.assets, function (a) {
|
|
var imgObj = new Image();
|
|
imgObj.src = a.src;
|
|
|
|
var x_px = _mapRef.x + (a.mx * TD.grid_size);
|
|
var y_px = _mapRef.y + (a.my * TD.grid_size);
|
|
var w_px = a.grid_x * TD.grid_size;
|
|
var h_px = a.grid_y * TD.grid_size;
|
|
|
|
var rebuiltAsset = {
|
|
id: a.id, src: a.src, img: imgObj,
|
|
mx: a.mx, my: a.my, grid_x: a.grid_x, grid_y: a.grid_y,
|
|
blocks_path: a.blocks_path, category: a.category,
|
|
x: x_px, y: y_px, width: w_px, height: h_px,
|
|
y2: y_px + h_px, is_asset: true
|
|
};
|
|
|
|
_mapRef.assets.push(rebuiltAsset);
|
|
|
|
// Re-apply pathing/building blockages for this asset
|
|
for (var dx = 0; dx < a.grid_x; dx++) {
|
|
for (var dy = 0; dy < a.grid_y; dy++) {
|
|
var g = _mapRef.getGrid(a.mx + dx, a.my + dy);
|
|
if (g) {
|
|
if (a.category !== "path") g.build_flag = 0;
|
|
if (a.blocks_path) g.passable_flag = 0;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
var _this = this;
|
|
if (cfg.grids_cfg) {
|
|
TD.lang.each(cfg.grids_cfg, function (obj) {
|
|
var grid = _this.getGrid(obj.pos[0], obj.pos[1]); if (!grid) return;
|
|
if (!isNaN(obj.passable_flag)) grid.passable_flag = obj.passable_flag;
|
|
if (!isNaN(obj.build_flag)) grid.build_flag = obj.build_flag;
|
|
if (obj.building) {
|
|
grid.addBuilding(obj.building);
|
|
if (obj.building_level && grid.building) {
|
|
for (var lvl = 1; lvl < obj.building_level; lvl++) grid.building.upgrade();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
},
|
|
checkHasWeapon: function () { this.has_weapon = (this.anyBuilding(function (obj) { return obj.is_weapon; }) != null); },
|
|
getGrid: function (mx, my) { var p = my * this.grid_x + mx; return this.grids[p]; },
|
|
anyMonster: function (f) { return TD.lang.any(this.monsters, f); },
|
|
anyBuilding: function (f) { return TD.lang.any(this.buildings, f); },
|
|
anyBullet: function (f) { return TD.lang.any(this.bullets, f); },
|
|
eachBuilding: function (f) { TD.lang.each(this.buildings, f); },
|
|
eachMonster: function (f) { TD.lang.each(this.monsters, f); },
|
|
eachBullet: function (f) { TD.lang.each(this.bullets, f); },
|
|
preBuild: function (building_type) {
|
|
TD.mode = "build"; if (this.pre_building) { this.pre_building.remove(); }
|
|
this.pre_building = new TD.Building(this.id + "-pre-building-" + TD.lang.rndStr(), { type: building_type, map: this, is_pre_building: true });
|
|
this.scene.addElement(this.pre_building, 1, this.render_level + 1);
|
|
},
|
|
cancelPreBuild: function () { TD.mode = "normal"; if (this.pre_building) { this.pre_building.remove(); } },
|
|
clearInvalidElements: function () {
|
|
if (this._wait_clearInvalidElements > 0) { this._wait_clearInvalidElements--; return; }
|
|
this._wait_clearInvalidElements = 20;
|
|
var a = []; TD.lang.shift(this.buildings, function (obj) { if (obj.is_valid) a.push(obj); }); this.buildings = a;
|
|
a = []; TD.lang.shift(this.monsters, function (obj) { if (obj.is_valid) a.push(obj); }); this.monsters = a;
|
|
a = []; TD.lang.shift(this.bullets, function (obj) { if (obj.is_valid) a.push(obj); }); this.bullets = a;
|
|
if (this.loots) { a = []; TD.lang.shift(this.loots, function (obj) { if (obj.is_valid) a.push(obj); }); this.loots = a; }
|
|
},
|
|
|
|
addMonster: function (monster) {
|
|
if (!this.entrances || this.entrances.length === 0) return;
|
|
|
|
var m_idx = typeof monster == "number" ? monster : monster.idx;
|
|
|
|
if (this.spawn_mode === "simultaneous") {
|
|
// Spawn a clone at EVERY entrance
|
|
for (var i = 0; i < this.entrances.length; i++) {
|
|
var new_m = new TD.Monster(null, { idx: m_idx, difficulty: TD.difficulty, step_level: this.step_level, render_level: this.render_level + 2 });
|
|
if (window.is_demo_mode) new_m.speed *= 2; // Demo mode 2x speed
|
|
this.entrances[i].addMonster(new_m);
|
|
}
|
|
} else {
|
|
// Spawn at a RANDOM entrance
|
|
var randEnt = this.entrances[Math.floor(Math.random() * this.entrances.length)];
|
|
var new_m = new TD.Monster(null, { idx: m_idx, difficulty: TD.difficulty, step_level: this.step_level, render_level: this.render_level + 2 });
|
|
if (window.is_demo_mode) new_m.speed *= 2; // Demo mode 2x speed
|
|
randEnt.addMonster(new_m);
|
|
}
|
|
},
|
|
|
|
addMonsters: function (n, monster) { this._wait_add_monsters = n; this._wait_add_monsters_objidx = monster; },
|
|
addMonsters2: function (arr) { this._wait_add_monsters_arr = arr; },
|
|
checkPassable: function (mx, my) { var grid = this.getGrid(mx, my); return (grid != null && grid.passable_flag == 1 && grid.build_flag == 0); },
|
|
step: function () {
|
|
this.clearInvalidElements();
|
|
|
|
// --- NEW: UNIVERSAL DEAD ZONE TRACKER ---
|
|
if (this.monsters.length > 0 && typeof TD.last_fire_frame !== 'undefined') {
|
|
// If 6 seconds (144 frames) pass with monsters on screen but NO shots fired
|
|
if (TD.iframe - TD.last_fire_frame >= (TD.exp_fps * 6)) {
|
|
if (typeof TD.logEvent === 'function' && window.IntelData) {
|
|
TD.logEvent(window.IntelData.getEvent("tactical_dead_zone", "GENERAL"));
|
|
}
|
|
// Reset the timer so it waits another 6 seconds before warning again
|
|
TD.last_fire_frame = TD.iframe;
|
|
}
|
|
}
|
|
|
|
// --- ENDLESS MODE DIFFICULTY SCALING ---
|
|
|
|
// If the map is completely clear of monsters and the queue is empty, the wave is over.
|
|
if (!window.is_campaign_mode && !window.is_demo_mode) {
|
|
if (this.monsters.length === 0 && this._wait_add_monsters === 0 && this._wait_add_monsters_arr.length === 0) {
|
|
// Prevent it from scaling repeatedly while waiting for the next wave to start
|
|
if (!this._did_scale_this_wave) {
|
|
TD.difficulty += 0.05; // 5% increase in Monster HP/Speed per wave
|
|
this._did_scale_this_wave = true;
|
|
}
|
|
} else {
|
|
this._did_scale_this_wave = false; // Reset lock once monsters start spawning again
|
|
}
|
|
}
|
|
|
|
if (this._wait_add_monsters > 0) { this.addMonster(this._wait_add_monsters_objidx); this._wait_add_monsters--; }
|
|
else if (this._wait_add_monsters_arr.length > 0) { var a = this._wait_add_monsters_arr.shift(); this.addMonsters(a[0], a[1]); }
|
|
|
|
// --- DEMO MODE TRICKLE SPAWN ---
|
|
|
|
if (window.is_demo_mode && this.entrance) {
|
|
// ~3.0% chance per frame to spawn 1-3 random enemies (Increased by 2x)
|
|
if (Math.random() < 0.03 && this._wait_add_monsters === 0 && this._wait_add_monsters_arr.length === 0) {
|
|
var randType = Math.floor(Math.random() * TD.monster_type_count);
|
|
var randCount = Math.floor(Math.random() * 3) + 1; // 1 to 3 monsters
|
|
this.addMonsters(randCount, randType);
|
|
}
|
|
|
|
// ~0.2% chance per frame to drop a massive Artillery Strike on a group of monsters
|
|
if (Math.random() < 0.002 && this.monsters.length > 0) {
|
|
// Pick a random monster as the target center (naturally biases towards dense groups)
|
|
var targetIdx = Math.floor(Math.random() * this.monsters.length);
|
|
var targetMonster = this.monsters[targetIdx];
|
|
|
|
if (targetMonster && targetMonster.is_valid) {
|
|
var rx = targetMonster.cx;
|
|
var ry = targetMonster.cy;
|
|
var blast_radius = TD.grid_size * 5;
|
|
TD.Explode("demo-arty-" + TD.lang.rndStr(), { cx: rx, cy: ry, r: blast_radius / _TD.retina, step_level: 1, render_level: 9, color: "#ff5500", scene: this.scene, time: 1.5 });
|
|
|
|
// Kill monsters caught in the targeted blast
|
|
TD.lang.each(this.monsters, function (m) {
|
|
if (m.is_valid && (Math.pow(m.cx - rx, 2) + Math.pow(m.cy - ry, 2) <= Math.pow(blast_radius, 2))) {
|
|
m.beHit({ killed: 0, gainXp: function(){} }, 500);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Trigger consumable timers
|
|
if (TD.stepConsumables) TD.stepConsumables();
|
|
},
|
|
|
|
|
|
render: function () {
|
|
var ctx = TD.ctx; ctx.strokeStyle = "#99a"; ctx.lineWidth = _TD.retina; ctx.beginPath(); ctx.strokeRect(this.x + 0.5, this.y + 0.5, this.width, this.height); ctx.closePath(); ctx.stroke();
|
|
|
|
var renderQueue = [];
|
|
|
|
// 1. Gather Assets & Draw Paths Immediately
|
|
if (this.assets) {
|
|
for (var i = 0; i < this.assets.length; i++) {
|
|
var a = this.assets[i];
|
|
if (a.category === "path") {
|
|
// Draw paths flat on the background immediately
|
|
if (a.img && a.img.complete) ctx.drawImage(a.img, a.x, a.y, a.width, a.height);
|
|
} else {
|
|
renderQueue.push(a); // Props go to Y-Sort queue
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Gather Buildings
|
|
for (var i = 0; i < this.buildings.length; i++) {
|
|
var b = this.buildings[i];
|
|
if (b.is_valid) renderQueue.push(b);
|
|
}
|
|
|
|
// 3. Gather Monsters
|
|
for (var i = 0; i < this.monsters.length; i++) {
|
|
var m = this.monsters[i];
|
|
if (m.is_valid) renderQueue.push(m);
|
|
}
|
|
|
|
// SORT the queue based on the bottom Y edge
|
|
renderQueue.sort(function(a, b) {
|
|
var a_bottom = a.y2 || (a.cy + (a.r || 0));
|
|
var b_bottom = b.y2 || (b.cy + (b.r || 0));
|
|
return a_bottom - b_bottom;
|
|
});
|
|
|
|
// DRAW everything in order from back to front
|
|
for (var i = 0; i < renderQueue.length; i++) {
|
|
var obj = renderQueue[i];
|
|
|
|
if (obj.is_asset) {
|
|
if (obj.img && obj.img.complete) ctx.drawImage(obj.img, obj.x, obj.y, obj.width, obj.height);
|
|
} else if (obj.is_monster) {
|
|
// Force the monster to render here
|
|
if (obj.render) obj.render.call(obj, "ysort"); // FIXED: Pass the required argument
|
|
} else {
|
|
|
|
if (obj.is_visiable && obj.wait_blink >= 0) {
|
|
TD.renderBuilding(obj);
|
|
if (obj.map.is_main_map && (obj.is_selected || obj.is_hover || obj.map.show_all_ranges) && obj.is_weapon && obj.range > 0 && obj.grid) {
|
|
ctx.lineWidth = _TD.retina; ctx.fillStyle = "rgba(187, 141, 32, 0.15)"; ctx.strokeStyle = "#bb8d20"; ctx.beginPath(); ctx.arc(obj.cx, obj.cy, obj.range_px, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// FINALLY: Redraw Entrance and Exit arrows so they are never hidden by assets
|
|
if (this.entrance) this.entrance.render();
|
|
if (this.exit) this.exit.render();
|
|
|
|
// DRAW MR. HANDY GHOST (Silhouette Stencil)
|
|
if (TD.mode === "deploy_handy" && TD.mouse_x != null && TD.mouse_y != null) {
|
|
var r = (TD.grid_size * 0.9) / 2;
|
|
if (!this._handy_ghost_img) { this._handy_ghost_img = new Image(); this._handy_ghost_img.src = "assets/images/mrhandy.png"; }
|
|
|
|
if (this._handy_ghost_img.complete) {
|
|
// Generate the stencil once and cache it
|
|
if (!this._handy_ghost_canvas) {
|
|
this._handy_ghost_canvas = document.createElement('canvas');
|
|
this._handy_ghost_canvas.width = r*2 + 4;
|
|
this._handy_ghost_canvas.height = r*2 + 4;
|
|
var oCtx = this._handy_ghost_canvas.getContext('2d');
|
|
var img = this._handy_ghost_img;
|
|
|
|
// 1. Draw 4 offsets to create a thicker border
|
|
oCtx.drawImage(img, 0, 2, r*2, r*2);
|
|
oCtx.drawImage(img, 4, 2, r*2, r*2);
|
|
oCtx.drawImage(img, 2, 0, r*2, r*2);
|
|
oCtx.drawImage(img, 2, 4, r*2, r*2);
|
|
|
|
// 2. Fill the expanded shape with solid green
|
|
oCtx.globalCompositeOperation = 'source-in';
|
|
oCtx.fillStyle = '#0f0';
|
|
oCtx.fillRect(0, 0, r*2 + 4, r*2 + 4);
|
|
|
|
// 3. Cut out the original image shape from the center, leaving only the outline
|
|
oCtx.globalCompositeOperation = 'destination-out';
|
|
oCtx.drawImage(img, 2, 2, r*2, r*2);
|
|
|
|
// 4. Create a translucent green fill using a temporary canvas
|
|
var c2 = document.createElement('canvas');
|
|
c2.width = r*2 + 4; c2.height = r*2 + 4;
|
|
var x2 = c2.getContext('2d');
|
|
x2.drawImage(img, 2, 2, r*2, r*2);
|
|
x2.globalCompositeOperation = 'source-in';
|
|
x2.fillStyle = 'rgba(0, 255, 0, 0.4)';
|
|
x2.fillRect(0, 0, r*2+4, r*2+4);
|
|
|
|
// 5. Draw the translucent fill back into the cut-out center
|
|
oCtx.globalCompositeOperation = 'source-over';
|
|
oCtx.drawImage(c2, 0, 0);
|
|
}
|
|
|
|
// Draw the perfectly generated stenciled ghost on the main canvas
|
|
ctx.drawImage(this._handy_ghost_canvas, TD.mouse_x - r - 2, TD.mouse_y - r - 2);
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
|
|
onOut: function () { if (this.is_main_map && this.pre_building) this.pre_building.hide(); }
|
|
};
|
|
TD.Map = function (id, cfg) {
|
|
cfg.on_events = ["enter", "out"]; var map = new TD.Element(id, cfg); TD.lang.mix(map, map_obj); map._init(cfg); return map;
|
|
};
|
|
var map_selecthl_obj = {
|
|
_init: function (cfg) { this.map = cfg.map; this.width = TD.grid_size + 2; this.height = TD.grid_size + 2; this.is_visiable = false; },
|
|
show: function (grid) { this.x = grid.x; this.y = grid.y; this.is_visiable = true; },
|
|
render: function () {
|
|
var ctx = TD.ctx; ctx.lineWidth = 2; ctx.strokeStyle = "#f93"; ctx.beginPath(); ctx.strokeRect(this.x, this.y, this.width - 1, this.height - 1); ctx.closePath(); ctx.stroke();
|
|
}
|
|
};
|
|
TD.MapSelectHighLight = function (id, cfg) {
|
|
var map_selecthl = new TD.Element(id, cfg); TD.lang.mix(map_selecthl, map_selecthl_obj); map_selecthl._init(cfg); return map_selecthl;
|
|
};
|
|
});
|
|
|
|
_TD.a.push(function (TD) {
|
|
var monster_obj = {
|
|
_init: function (cfg) {
|
|
cfg = cfg || {}; this.is_monster = true; this.idx = cfg.idx || 1; this.difficulty = cfg.difficulty || 1.0;
|
|
var attr = TD.getDefaultMonsterAttributes(this.idx);
|
|
this.name = attr.name || "Unknown"; // NEW: Store the name so VATS tooltip can read it without crashing
|
|
this.is_flying = !!attr.is_flying;
|
|
this.is_stealthed = !!attr.is_stealthed;
|
|
this.speed = Math.floor((attr.speed + this.difficulty / 2) * (Math.random() * 0.5 + 0.75));
|
|
if (this.speed < 1) this.speed = 1; if (this.speed > cfg.max_speed) this.speed = cfg.max_speed;
|
|
// Universally increased HP modifier (changed from 0.5 to 1.5 for a massive 300% boost)
|
|
this.life = this.life0 = Math.floor(attr.life * (this.difficulty + 1) * (Math.random() + 0.5) * 1.5);
|
|
if (this.life < 1) this.life = this.life0 = 1;
|
|
this.shield = Math.floor(attr.shield + this.difficulty / 2); if (this.shield < 0) this.shield = 0;
|
|
this.damage = Math.floor((attr.damage || 1) * (Math.random() * 0.5 + 0.75)); if (this.damage < 1) this.damage = 1;
|
|
this.money = attr.money || Math.floor(Math.sqrt((this.speed + this.life) * (this.shield + 1) * this.damage)); if (this.money < 1) this.money = 1;
|
|
this.color = attr.color || TD.lang.rndRGB(); this.r = Math.floor(this.damage * 3.6) * _TD.retina;
|
|
if (this.r < (12 * _TD.retina)) this.r = 12 * _TD.retina; if (this.r > TD.grid_size - (4 * _TD.retina)) this.r = TD.grid_size - (4 * _TD.retina);
|
|
this.render = attr.render; this.grid = null; this.map = null; this.next_grid = null; this.way = []; this.toward = 2; this._dx = 0; this._dy = 0; this.is_blocked = false;
|
|
|
|
// NEW: Initial random lane assignments & Preferred drift target
|
|
this.offset_x = (Math.random() * (TD.grid_size * 0.6)) - (TD.grid_size * 0.3);
|
|
this.offset_y = (Math.random() * (TD.grid_size * 0.6)) - (TD.grid_size * 0.3);
|
|
this.preferred_offset_x = this.offset_x;
|
|
this.preferred_offset_y = this.offset_y;
|
|
|
|
// MOLE RAT: Initial burrow state
|
|
if (this.name === "Mole Rat") {
|
|
this.is_burrowing = true;
|
|
this.is_stealthed = true;
|
|
this.state_timer = TD.exp_fps * (4 + Math.random() * 2); // 4-6 seconds initially
|
|
}
|
|
|
|
// ENDLESS SCALING: +2% HP every 2 waves, +0.6% Speed every 3 waves (Caps at Wave 40)
|
|
var current_wave = (TD.stage && TD.stage.current_act && TD.stage.current_act.current_scene) ? TD.stage.current_act.current_scene.wave : 1;
|
|
if (current_wave > 10) {
|
|
var past_10 = current_wave - 10;
|
|
var stat_waves = Math.min(past_10, 30); // Locks scaling at 30 waves past 10 (Wave 40)
|
|
|
|
var hp_mult = 1.0 + (Math.floor(stat_waves / 2) * 0.02);
|
|
var spd_mult = 1.0 + (Math.floor(stat_waves / 3) * 0.006);
|
|
|
|
this.life0 = Math.floor(this.life0 * hp_mult);
|
|
this.life = this.life0;
|
|
this.speed = this.speed * spd_mult;
|
|
}
|
|
},
|
|
|
|
|
|
caculatePos: function () { var r = this.r; this.x = this.cx - r; this.y = this.cy - r; this.x2 = this.cx + r; this.y2 = this.cy + r; },
|
|
beHit: function (building, damage) {
|
|
if (!this.is_valid) return;
|
|
|
|
// --- NEW: Vulnerability Aura Check ---
|
|
var _this = this;
|
|
var vuln_mult = 1.0;
|
|
if (this.map && this.map.buildings) {
|
|
TD.lang.each(this.map.buildings, function(b) {
|
|
if (b.is_valid && b.aura_vulnerability) {
|
|
var dist = Math.pow(b.cx - _this.cx, 2) + Math.pow(b.cy - _this.cy, 2);
|
|
if (dist <= Math.pow(b.range_px, 2)) vuln_mult = b.aura_vulnerability; // Usually 1.15
|
|
}
|
|
});
|
|
}
|
|
damage = Math.floor(damage * vuln_mult);
|
|
|
|
var min_damage = Math.ceil(damage * 0.1); damage -= this.shield; if (damage <= min_damage) damage = min_damage;
|
|
this.life -= damage;
|
|
var scoreGained = Math.floor(Math.sqrt(damage));
|
|
|
|
TD.score += scoreGained;
|
|
|
|
// Add a very small trickle to V.A.T.S. charge on hit
|
|
if (typeof TD.vats_charge !== 'undefined' && !TD.vats_active) {
|
|
TD.vats_charge += (scoreGained * 0.1); // Reduced from 0.5 to prevent massive spikes
|
|
if (TD.vats_charge > TD.vats_max_charge) TD.vats_charge = TD.vats_max_charge;
|
|
}
|
|
|
|
// --- NEW: Award XP just for hitting the target (2 to 5 XP) ---
|
|
if (building && building.gainXp) {
|
|
var hitXp = Math.floor(Math.random() * 4) + 2;
|
|
building.gainXp(hitXp);
|
|
}
|
|
|
|
|
|
// NEW: Scatter and Throttle (Only show text every 3 frames per monster)
|
|
if (!this._last_dmg_frame || TD.iframe - this._last_dmg_frame > 3) {
|
|
|
|
this._last_dmg_frame = TD.iframe;
|
|
var offsetX = (Math.random() * 24) - 12; // Scatters horizontally
|
|
var offsetY = (Math.random() * 10) - 5; // Scatters slightly vertically
|
|
|
|
new TD.FloatingText("dmg-" + TD.lang.rndStr(), {
|
|
cx: this.cx + offsetX, cy: (this.cy - 10) + offsetY, text: "-" + damage, color: "#ff5555", map: this.map, size: 10, life: 16
|
|
});
|
|
}
|
|
|
|
if (this.life <= 0) {
|
|
|
|
|
|
// --- NEW: Tiered XP for landing the killing blow ---
|
|
if (building && building.gainXp) {
|
|
// Base kill XP is 10. Max index is 8 (Sentry Bot).
|
|
// The formula gives roughly 10 to 25 XP based on how tough the monster is.
|
|
var killXp = 10 + Math.floor(this.idx * 1.8);
|
|
building.gainXp(killXp);
|
|
}
|
|
|
|
// NEW: Scaled Chance to drop Loot (Base 5% + 1.5% per monster index level)
|
|
var base_chance = 0.05;
|
|
var scaling_bonus = (this.idx * 0.015);
|
|
var dropChance = (window.devForcedDrop && window.devForcedDrop !== "none") ? 1.0 : (base_chance + scaling_bonus);
|
|
|
|
// GUARANTEED WAVE DROPS (First kill of the specific wave)
|
|
|
|
var forceLoot = null;
|
|
if (this.scene && this.scene.wave === 3 && !this.scene._dropW3) {
|
|
forceLoot = "money"; this.scene._dropW3 = true;
|
|
} else if (this.scene && this.scene.wave === 7 && !this.scene._dropW7) {
|
|
forceLoot = "stimpak"; this.scene._dropW7 = true;
|
|
} else if (this.scene && this.scene.wave === 15 && !this.scene._dropW15) {
|
|
forceLoot = "stimpak"; this.scene._dropW15 = true;
|
|
}
|
|
|
|
if (forceLoot || Math.random() < dropChance) {
|
|
var newLoot = new TD.Loot("loot-" + TD.lang.rndStr(), {
|
|
cx: this.cx,
|
|
cy: this.cy,
|
|
map: this.map,
|
|
monster_bounty: this.money // scale with monster's actual worth
|
|
});
|
|
|
|
if (forceLoot) {
|
|
newLoot.loot_type = forceLoot;
|
|
if (forceLoot === "money") newLoot.value = 100; // Extra caps for the guaranteed drop
|
|
}
|
|
}
|
|
|
|
this.beKilled(building);
|
|
|
|
}
|
|
|
|
var balloontip = this.scene.panel.balloontip;
|
|
if (balloontip.el == this) {
|
|
var tags = [];
|
|
if (this.is_flying) tags.push("[FLYING]");
|
|
if (this.is_stealthed) tags.push("[STEALTH]");
|
|
if (this.speed >= 15) tags.push("[FAST]");
|
|
if (this.life0 >= 500) tags.push("[VERY STRONG]");
|
|
if (this.life0 >= 2000) tags.push("[BOSS]");
|
|
var tagStr = tags.length > 0 ? "\n" + tags.join(" ") : "";
|
|
balloontip.text = this.name.toUpperCase() + "\nHP: " + this.life + " / " + this.life0 + tagStr;
|
|
}
|
|
},
|
|
|
|
beKilled: function (building) {
|
|
if (!this.is_valid) return;
|
|
this.life = 0; this.is_valid = false; building.killed++;
|
|
|
|
// --- NEW: ANNOUNCE KILL ---
|
|
if (window.IntelData && TD.Announcer) {
|
|
// Was it a clutch kill right near the exit? (Calculate distance from exit grid)
|
|
var clutch = false;
|
|
if (this.map && this.map.exit) {
|
|
var distToExit = Math.sqrt(Math.pow(this.cx - this.map.exit.cx, 2) + Math.pow(this.cy - this.map.exit.cy, 2));
|
|
if (distToExit < (TD.grid_size * 2)) clutch = true; // Within 2 grid squares!
|
|
}
|
|
|
|
if (clutch) {
|
|
// Highest priority, absolute clutch save!
|
|
TD.Announcer.add("CRITICAL SAVE: " + this.name.toUpperCase() + " ELIMINATED AT THE PERIMETER BY " + (building ? building.name.toUpperCase() : "DEFENSES"), 5);
|
|
} else if (this.life0 >= 2000) {
|
|
// Boss Kill!
|
|
TD.Announcer.add(window.IntelData.getEvent("kill", TD.current_faction, null, this.name), 5);
|
|
} else if (Math.random() < 0.15) {
|
|
// Normal Kill (Throttled so swarms don't spam)
|
|
TD.Announcer.add(window.IntelData.getEvent("kill", TD.current_faction, null, this.name), 2);
|
|
}
|
|
}
|
|
|
|
// --- NEW: Scaling Kill Bounty ---
|
|
|
|
var wave_bonus = 0;
|
|
var current_wave = 1;
|
|
if (this.scene && this.scene.wave) current_wave = this.scene.wave;
|
|
|
|
if (current_wave >= 5) wave_bonus = 5;
|
|
if (current_wave >= 9) wave_bonus = 8;
|
|
if (current_wave >= 13) wave_bonus = 10;
|
|
if (current_wave >= 17) wave_bonus = 15;
|
|
|
|
var final_bounty = this.money + wave_bonus;
|
|
TD.money += final_bounty;
|
|
|
|
// --- NEW: BONUS BOUNTY PAYOUT ---
|
|
if (building && building.bonus_bounty) {
|
|
var extraCaps = Math.floor(Math.random() * 6) + 15; // Random 15 to 20
|
|
TD.money += extraCaps;
|
|
new TD.FloatingText("bounty-" + TD.lang.rndStr(), {
|
|
cx: this.cx, cy: this.cy - 20, text: "+" + extraCaps + " CAPS", color: "#ffff00", map: this.map, size: 14, life: 36
|
|
});
|
|
}
|
|
|
|
// --- NEW: V.A.T.S. Charge Generation ---
|
|
|
|
if (typeof TD.vats_charge !== 'undefined' && !TD.vats_active) {
|
|
// Fixed kill charge: base 3, scales up slightly for stronger enemies
|
|
TD.vats_charge += 3 + Math.floor(this.idx * 1.5);
|
|
if (TD.vats_charge > TD.vats_max_charge) TD.vats_charge = TD.vats_max_charge;
|
|
}
|
|
|
|
|
|
// --- NEW: Shatter AoE ---
|
|
if (this.is_frozen && building && building.shatter_aoe) {
|
|
var blast_radius = TD.grid_size * 2.0;
|
|
TD.Explode("shatter-" + TD.lang.rndStr(), { cx: this.cx, cy: this.cy, r: blast_radius / _TD.retina, step_level: 1, render_level: 9, color: "#aaffff", scene: this.grid.scene, time: 0.3 });
|
|
var _this = this;
|
|
TD.lang.each(this.map.monsters, function(m) {
|
|
if (m.is_valid && m !== _this && Math.pow(m.cx - _this.cx, 2) + Math.pow(m.cy - _this.cy, 2) <= Math.pow(blast_radius, 2)) {
|
|
m.beHit(building, 50); // Deal 50 splash damage
|
|
}
|
|
});
|
|
}
|
|
|
|
TD.Explode(this.id + "-explode", { cx: this.cx, cy: this.cy, color: this.color, r: this.r, step_level: this.step_level, render_level: this.render_level, scene: this.grid.scene });
|
|
},
|
|
|
|
arrive: function () { this.grid = this.next_grid; this.next_grid = null; this.checkFinish(); },
|
|
findWay: function () {
|
|
var _this = this, bestWay = [], minLen = 999999;
|
|
for (var i = 0; i < this.map.exits.length; i++) {
|
|
var ext = this.map.exits[i];
|
|
var fw = new TD.FindWay(this.map.grid_x, this.map.grid_y, this.grid.mx, this.grid.my, ext.mx, ext.my, function (x, y) { return _this.map.checkPassable(x, y); });
|
|
if (fw.is_arrived && fw.way.length < minLen) { minLen = fw.way.length; bestWay = fw.way; }
|
|
}
|
|
this.way = bestWay;
|
|
},
|
|
checkFinish: function () {
|
|
if (this.grid && this.map && this.grid.is_exit) {
|
|
TD.wave_damage += this.damage;
|
|
|
|
if (window.is_demo_mode) {
|
|
this.pause(); this.del();
|
|
return;
|
|
}
|
|
|
|
// Trigger Screen Shake visually on the board container
|
|
var board = document.getElementById("td-board");
|
|
if (board) {
|
|
board.classList.remove("shake-active");
|
|
void board.offsetWidth; // Reflow to allow rapid sequential shakes
|
|
board.classList.add("shake-active");
|
|
}
|
|
|
|
if (this.map.map_type === "siege") {
|
|
TD.base_health -= (this.damage * 10);
|
|
if (TD.base_health <= 0) { TD.base_health = 0; TD.stage.gameover(); } else { this.pause(); this.del(); }
|
|
} else {
|
|
TD.life -= this.damage;
|
|
if (TD.life <= 0) { TD.life = 0; TD.stage.gameover(); } else { this.pause(); this.del(); }
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
beAddToGrid: function (grid) { this.grid = grid; this.map = grid.map; this.cx = grid.cx; this.cy = grid.cy; this.grid.scene.addElement(this); },
|
|
getNextGrid: function () {
|
|
if (this.way.length == 0 || Math.random() < 0.1) { this.findWay(); }
|
|
var next_grid = this.way.shift();
|
|
if (next_grid && !this.map.checkPassable(next_grid[0], next_grid[1])) { this.findWay(); next_grid = this.way.shift(); }
|
|
if (!next_grid) return;
|
|
this.next_grid = this.map.getGrid(next_grid[0], next_grid[1]);
|
|
},
|
|
chkIfBlocked: function (mx, my) {
|
|
var _this = this, canReachAnyExit = false;
|
|
for (var i = 0; i < this.map.exits.length; i++) {
|
|
var ext = this.map.exits[i];
|
|
var fw = new TD.FindWay(this.map.grid_x, this.map.grid_y, this.grid.mx, this.grid.my, ext.mx, ext.my, function (x, y) { return !(x == mx && y == my) && _this.map.checkPassable(x, y); });
|
|
if (!fw.is_blocked) { canReachAnyExit = true; break; }
|
|
}
|
|
return !canReachAnyExit;
|
|
},
|
|
beBlocked: function () { if (this.is_blocked) return; this.is_blocked = true; },
|
|
step: function () {
|
|
if (!this.is_valid || this.is_paused || !this.grid) return;
|
|
|
|
// NEW: Handle Freeze Debuff thawing
|
|
if (this.is_frozen) {
|
|
this.freeze_timer--;
|
|
|
|
if (this.freeze_timer > 0) {
|
|
// --- NEW: True Freeze Bypass ---
|
|
// If they have more than 5 seconds (120 frames) left, they are SOLID ICE.
|
|
// We simply return out of the function so they don't move or attack at all!
|
|
if (this.freeze_timer > 120) {
|
|
return;
|
|
}
|
|
|
|
// Dynamic Thaw (Last 5 seconds):
|
|
// Timer goes from 120 down to 0.
|
|
var freezePercentRemaining = this.freeze_timer / 120; // 1.0 down to 0.0
|
|
var thawPercent = 1 - freezePercentRemaining; // 0.0 up to 1.0
|
|
|
|
// They regain speed linearly as the timer ticks down.
|
|
this.speed = Math.max(0.1, this.original_speed * thawPercent);
|
|
|
|
} else {
|
|
// Done freezing
|
|
this.is_frozen = false;
|
|
this.speed = this.original_speed;
|
|
}
|
|
}
|
|
|
|
|
|
if (!this.next_grid) {
|
|
this.getNextGrid();
|
|
if (!this.next_grid) {
|
|
if (this.map.map_type === "siege") {
|
|
// NEW: Siege Mode Attack Logic
|
|
if (!this.attack_cooldown) this.attack_cooldown = 0;
|
|
|
|
if (this.attack_cooldown > 0) {
|
|
this.attack_cooldown--;
|
|
} else {
|
|
// Look for adjacent blocking towers
|
|
var adj = [
|
|
this.map.getGrid(this.grid.mx + 1, this.grid.my),
|
|
this.map.getGrid(this.grid.mx - 1, this.grid.my),
|
|
this.map.getGrid(this.grid.mx, this.grid.my + 1),
|
|
this.map.getGrid(this.grid.mx, this.grid.my - 1)
|
|
];
|
|
|
|
for (var i = 0; i < adj.length; i++) {
|
|
if (adj[i] && adj[i].building) {
|
|
var target = adj[i].building;
|
|
var dmg = this.damage || 5;
|
|
|
|
if (target.shield > 0) {
|
|
target.shield -= dmg;
|
|
if (target.shield < 0) target.shield = 0;
|
|
} else {
|
|
target.life -= dmg;
|
|
}
|
|
|
|
// Visual attack feedback
|
|
TD.Explode(this.id + "-atk", { cx: target.cx, cy: target.cy, r: 10 * _TD.retina, step_level: this.step_level, render_level: 9, color: "#ff0000", scene: this.map.scene, time: 0.2 });
|
|
|
|
if (target.life <= 0) target.remove(); // Destroy the building
|
|
|
|
this.attack_cooldown = 24; // Attack roughly once per second
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
this.beBlocked();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// --- NEW: Soft Swarm Repulsion & Lane Shifting ---
|
|
var _this = this;
|
|
var is_bumping = false; // Track if actively touching someone
|
|
TD.lang.each(this.map.monsters, function(m) {
|
|
if (m !== _this && m.is_valid && m.grid) {
|
|
var dx = _this.cx - m.cx;
|
|
var dy = _this.cy - m.cy;
|
|
var dist = Math.sqrt(dx*dx + dy*dy);
|
|
var minDist = _this.r + m.r;
|
|
|
|
if (dist > 0 && dist < minDist) {
|
|
is_bumping = true;
|
|
var push = (minDist - dist) * 0.15; // Tripled magnetic push
|
|
_this.offset_x += (dx / dist) * push;
|
|
_this.offset_y += (dy / dist) * push;
|
|
}
|
|
}
|
|
});
|
|
|
|
// --- NEW: Occasional random wandering ---
|
|
// ~1% chance per frame to decide to wander to a new part of the path
|
|
if (Math.random() < 0.01) {
|
|
this.preferred_offset_x = (Math.random() * (TD.grid_size * 0.7)) - (TD.grid_size * 0.35);
|
|
this.preferred_offset_y = (Math.random() * (TD.grid_size * 0.7)) - (TD.grid_size * 0.35);
|
|
}
|
|
|
|
// Smoothly pull toward PREFERRED lane over time if not bumping
|
|
if (!is_bumping) {
|
|
this.offset_x += (this.preferred_offset_x - this.offset_x) * 0.05;
|
|
this.offset_y += (this.preferred_offset_y - this.offset_y) * 0.05;
|
|
}
|
|
|
|
// Wider clamping (50%) so they don't clip entirely off the tile
|
|
var maxOff = TD.grid_size * 0.5;
|
|
if (this.offset_x > maxOff) this.offset_x = maxOff; else if (this.offset_x < -maxOff) this.offset_x = -maxOff;
|
|
if (this.offset_y > maxOff) this.offset_y = maxOff; else if (this.offset_y < -maxOff) this.offset_y = -maxOff;
|
|
|
|
// Add the dynamic lane offset to their target destination
|
|
var target_cx = this.next_grid.cx + this.offset_x;
|
|
var target_cy = this.next_grid.cy + this.offset_y;
|
|
|
|
// --- NEW: Smooth Vector Movement ---
|
|
var dpx = target_cx - this.cx;
|
|
var dpy = target_cy - this.cy;
|
|
var distToTarget = Math.sqrt(dpx * dpx + dpy * dpy);
|
|
|
|
// --- NEW: PATH PROGRESS ANNOUNCER ---
|
|
// Calculate how far they are based on their remaining path length
|
|
if (this.way && this.map && this.map.entrance && this.map.exit && window.IntelData && TD.Announcer) {
|
|
// Estimate total path length (just once)
|
|
if (!this._total_path_len) this._total_path_len = this.way.length + 1;
|
|
|
|
var percentTraveled = 100 - Math.floor((this.way.length / this._total_path_len) * 100);
|
|
|
|
// Trigger warnings at 50% and 75% marks
|
|
if (percentTraveled >= 50 && !this._warned_50) {
|
|
this._warned_50 = true;
|
|
// Only 10% chance to warn for generic mobs to prevent spam, 100% chance for Bosses
|
|
if (this.life0 >= 2000 || Math.random() < 0.1) {
|
|
TD.Announcer.add(window.IntelData.getEvent("path_progress", TD.current_faction, null, this.name, 50), 4);
|
|
}
|
|
}
|
|
if (percentTraveled >= 75 && !this._warned_75) {
|
|
this._warned_75 = true;
|
|
if (this.life0 >= 2000 || Math.random() < 0.2) {
|
|
TD.Announcer.add(window.IntelData.getEvent("path_progress", TD.current_faction, null, this.name, 75), 5);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- STATUS EFFECT TIMERS ---
|
|
|
|
if (typeof this.stealth_alpha === 'undefined') this.stealth_alpha = 0.0;
|
|
|
|
if (this.reveal_timer > 0) {
|
|
this.reveal_timer--;
|
|
if (this.reveal_timer <= 0) this.is_revealed = false;
|
|
}
|
|
|
|
if (this.is_poisoned) {
|
|
this.poison_timer--;
|
|
if (this.poison_timer <= 0) this.is_poisoned = false;
|
|
|
|
// Tick Damage every half second (12 frames)
|
|
if (this.poison_timer % 12 === 0) {
|
|
var pDmg = this.poison_damage || 5;
|
|
this.life -= pDmg;
|
|
new TD.FloatingText("pdmg-" + TD.lang.rndStr(), { cx: this.cx + (Math.random()*10-5), cy: this.cy - 10, text: "-" + pDmg, color: "#ff8800", map: this.map, size: 10, life: 12 });
|
|
if (this.life <= 0) this.beKilled({ killed: 0, gainXp: function(){} });
|
|
}
|
|
}
|
|
|
|
// Smooth Fading Math for Stealth/Burrowing
|
|
if (this.is_revealed || (!this.is_stealthed && !this.is_burrowing)) {
|
|
this.stealth_alpha += 0.05; // Fade In
|
|
if (this.stealth_alpha > 1.0) this.stealth_alpha = 1.0;
|
|
} else {
|
|
this.stealth_alpha -= 0.05; // Fade Out
|
|
if (this.stealth_alpha < 0.0) this.stealth_alpha = 0.0;
|
|
}
|
|
|
|
if (this.slow_timer > 0) {
|
|
this.slow_timer--;
|
|
if (this.slow_timer <= 0) this.is_slowed = false;
|
|
}
|
|
|
|
|
|
// MOLE RAT: State Machine (Burrowing vs Running)
|
|
if (typeof this.dig_timer === 'undefined') this.dig_timer = 0;
|
|
if (this.dig_timer > 0) this.dig_timer--;
|
|
|
|
if (this.name === "Mole Rat") {
|
|
this.state_timer--;
|
|
if (this.state_timer <= 0) {
|
|
this.is_burrowing = !this.is_burrowing;
|
|
this.dig_timer = TD.exp_fps * 1.0;
|
|
if (this.is_burrowing) {
|
|
this.is_stealthed = true;
|
|
this.is_revealed = false;
|
|
this.state_timer = TD.exp_fps * (4 + Math.random() * 2);
|
|
} else {
|
|
this.is_stealthed = false;
|
|
this.state_timer = TD.exp_fps * (6 + Math.random() * 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- NEW: Process Environmental Auras ---
|
|
var aura_slow_mult = 1.0;
|
|
if (this.map && this.map.buildings) {
|
|
TD.lang.each(this.map.buildings, function(b) {
|
|
if (b.is_valid) {
|
|
var dist2 = Math.pow(b.cx - _this.cx, 2) + Math.pow(b.cy - _this.cy, 2);
|
|
if (dist2 <= Math.pow(b.range_px, 2)) {
|
|
if (b.aura_slow) aura_slow_mult = Math.min(aura_slow_mult, b.aura_slow); // Apply the strongest slow (lowest mult)
|
|
if (b.aura_dot && TD.iframe % 30 === 0) {
|
|
_this.beHit(b, b.aura_dot); // Apply flat damage twice a second
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Base speed calculation
|
|
var current_speed = this.speed * TD.global_speed;
|
|
|
|
// Speed Modifiers
|
|
if (this.dig_timer > 0) {
|
|
current_speed = 0; // Frozen completely while digging up/down
|
|
} else {
|
|
current_speed *= aura_slow_mult; // Apply Radar Jammer Slow
|
|
if (this.is_slowed) current_speed *= 0.50; // 50% Penalty from Cryo towers
|
|
if (this.name === "Mole Rat" && this.is_burrowing) current_speed *= 0.25; // 75% slower underground
|
|
if (is_bumping) current_speed *= 1.30; // 30% speed boost if squeezing past someone
|
|
}
|
|
|
|
if (this.dig_timer > 0) {
|
|
current_speed = 0; // Frozen completely while digging up/down
|
|
} else {
|
|
if (this.is_slowed) current_speed *= 0.50; // 50% Penalty from Cryo towers
|
|
if (this.name === "Mole Rat" && this.is_burrowing) current_speed *= 0.25; // 75% slower underground
|
|
if (is_bumping) current_speed *= 1.30; // 30% speed boost if squeezing past someone
|
|
}
|
|
|
|
if (distToTarget <= current_speed) {
|
|
this.cx = target_cx;
|
|
this.cy = target_cy;
|
|
this.arrive();
|
|
} else {
|
|
|
|
this.cx += (dpx / distToTarget) * current_speed;
|
|
this.cy += (dpy / distToTarget) * current_speed;
|
|
}
|
|
|
|
this.caculatePos();
|
|
},
|
|
onEnter: function () {
|
|
var balloontip = this.scene.panel.balloontip;
|
|
if (balloontip.el == this) { balloontip.hide(); balloontip.el = null; }
|
|
else {
|
|
var tags = [];
|
|
if (this.is_flying) tags.push("[FLYING]");
|
|
if (this.is_stealthed) tags.push("[STEALTH]");
|
|
if (this.speed >= 15) tags.push("[FAST]");
|
|
if (this.life0 >= 500) tags.push("[VERY STRONG]");
|
|
if (this.life0 >= 2000) tags.push("[BOSS]");
|
|
|
|
var tagStr = tags.length > 0 ? "\n" + tags.join(" ") : "";
|
|
var msg = this.name.toUpperCase() + "\nHP: " + this.life + " / " + this.life0 + tagStr;
|
|
balloontip.msg(msg, this);
|
|
}
|
|
},
|
|
onOut: function () {
|
|
|
|
// FIXED: Tell the global UI to hide the balloon when mouse leaves
|
|
if (this.scene.panel.balloontip.el === this) {
|
|
this.scene.panel.balloontip.hide();
|
|
this.scene.panel.balloontip.el = null;
|
|
}
|
|
}
|
|
};
|
|
TD.Monster = function (id, cfg) { cfg.on_events = ["enter", "out"]; var monster = new TD.Element(id, cfg); TD.lang.mix(monster, monster_obj); monster._init(cfg); return monster; };
|
|
|
|
var explode_obj = {
|
|
_init: function (cfg) {
|
|
cfg = cfg || {}; var rgb = TD.lang.rgb2Arr(cfg.color);
|
|
this.cx = cfg.cx; this.cy = cfg.cy; this.r = cfg.r * _TD.retina; this.step_level = cfg.step_level; this.render_level = cfg.render_level;
|
|
this.rgb_r = rgb[0]; this.rgb_g = rgb[1]; this.rgb_b = rgb[2]; this.rgb_a = 1; this.wait = this.wait0 = TD.exp_fps * (cfg.time || 1);
|
|
cfg.scene.addElement(this);
|
|
},
|
|
|
|
step: function () { if (!this.is_valid) return; this.wait--; this.r++; this.is_valid = this.wait > 0; this.rgb_a = this.wait / this.wait0; },
|
|
render: function () {
|
|
var ctx = TD.ctx; ctx.fillStyle = "rgba(" + this.rgb_r + "," + this.rgb_g + "," + this.rgb_b + "," + this.rgb_a + ")";
|
|
ctx.beginPath(); ctx.arc(this.cx, this.cy, this.r, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill();
|
|
}
|
|
};
|
|
TD.Explode = function (id, cfg) { var explode = new TD.Element(id, cfg); TD.lang.mix(explode, explode_obj); explode._init(cfg); return explode; };
|
|
|
|
// --- NEW: Expanding Hollow Radar Pulse Effect ---
|
|
var radar_pulse_obj = {
|
|
_init: function (cfg) {
|
|
cfg = cfg || {}; var rgb = TD.lang.rgb2Arr(cfg.color);
|
|
this.cx = cfg.cx; this.cy = cfg.cy; this.map = cfg.map || cfg.scene.map;
|
|
this.max_r = cfg.r * _TD.retina;
|
|
this.r = 1; // Start tiny at the center of the tower
|
|
this.step_level = cfg.step_level; this.render_level = cfg.render_level;
|
|
this.rgb_r = rgb[0]; this.rgb_g = rgb[1]; this.rgb_b = rgb[2]; this.rgb_a = 1;
|
|
this.wait = this.wait0 = TD.exp_fps * (cfg.time || 1);
|
|
this.dr = this.max_r / this.wait0; // Calculate expansion speed per frame
|
|
cfg.scene.addElement(this);
|
|
},
|
|
step: function () {
|
|
if (!this.is_valid) return;
|
|
this.wait--;
|
|
this.r += this.dr; // Expand radius outward
|
|
|
|
var _this = this;
|
|
if (this.map && this.map.monsters) {
|
|
TD.lang.each(this.map.monsters, function(m) {
|
|
if (m.is_valid && (m.is_stealthed || m.is_burrowing)) {
|
|
var dist = Math.sqrt(Math.pow(m.cx - _this.cx, 2) + Math.pow(m.cy - _this.cy, 2));
|
|
|
|
// BULLETPROOF: If the expanding ring has reached the monster at all
|
|
if (dist <= (_this.r + m.r)) {
|
|
// Stay revealed for 3.2 seconds (fully fades out right as the next pulse hits them)
|
|
m.reveal_timer = Math.floor(TD.exp_fps * 3.2);
|
|
m.is_revealed = true;
|
|
}
|
|
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
this.is_valid = this.wait > 0;
|
|
this.rgb_a = this.wait / this.wait0; // Fade out as it reaches the edge
|
|
},
|
|
|
|
render: function () {
|
|
var ctx = TD.ctx;
|
|
// Draw ONLY the hollow stroke, NOT a solid fill!
|
|
ctx.strokeStyle = "rgba(" + this.rgb_r + "," + this.rgb_g + "," + this.rgb_b + "," + this.rgb_a + ")";
|
|
ctx.lineWidth = 4 * _TD.retina;
|
|
ctx.beginPath(); ctx.arc(this.cx, this.cy, this.r, 0, Math.PI * 2, true); ctx.closePath(); ctx.stroke();
|
|
}
|
|
};
|
|
TD.RadarPulse = function (id, cfg) { var pulse = new TD.Element(id, cfg); TD.lang.mix(pulse, radar_pulse_obj); pulse._init(cfg); return pulse; };
|
|
|
|
});
|
|
|
|
|
|
_TD.a.push(function (TD) {
|
|
var panel_obj = {
|
|
_init: function (cfg) {
|
|
cfg = cfg || {}; this.x = cfg.x; this.y = cfg.y; this.scene = cfg.scene; this.map = cfg.main_map;
|
|
this.addToScene(this.scene, 1, 7);
|
|
|
|
// Dummy map so toggleSelected doesn't crash when it searches for the old panel_map
|
|
this.scene.panel_map = { eachBuilding: function() {} };
|
|
|
|
this.gameover_obj = new TD.GameOver("panel-gameover", { panel: this, scene: this.scene, step_level: this.step_level, is_visiable: false, x: 0, y: 0, width: this.scene.stage.width, height: this.scene.stage.height, render_level: 9 });
|
|
this.balloontip = new TD.BalloonTip("panel-balloon-tip", { scene: this.scene, step_level: this.step_level, render_level: 9 }); this.balloontip.addToScene(this.scene, 1, 9);
|
|
|
|
var _this = this;
|
|
var makeBtn = function(id) {
|
|
var el = document.getElementById(id);
|
|
return {
|
|
el: el,
|
|
show: function() { if(this.el) this.el.style.display = "inline-block"; },
|
|
hide: function() { if(this.el) this.el.style.display = "none"; },
|
|
set desc(val) { var descEl = document.getElementById("ui-action-desc"); if(descEl) descEl.innerText = val; }
|
|
};
|
|
};
|
|
this.btn_pause = makeBtn("ui-btn-pause");
|
|
this.btn_restart = makeBtn("ui-btn-restart");
|
|
this.btn_upgrade = makeBtn("ui-btn-upgrade");
|
|
this.btn_sell = makeBtn("ui-btn-sell");
|
|
this.btn_build = makeBtn("ui-btn-build");
|
|
|
|
var btnPause = document.getElementById("ui-btn-pause");
|
|
if (btnPause) btnPause.onclick = function() {
|
|
if (_this.scene.state == 1) {
|
|
_this.scene.pause();
|
|
this.innerText = "CONTINUE";
|
|
_this.btn_upgrade.hide(); _this.btn_sell.hide(); _this.btn_build.hide(); _this.btn_restart.show();
|
|
document.body.classList.add("is-paused");
|
|
}
|
|
else if (_this.scene.state == 2) {
|
|
_this.scene.start();
|
|
this.innerText = "PAUSE";
|
|
_this.btn_restart.hide();
|
|
if (_this.scene.map.selected_building) { _this.btn_upgrade.show(); _this.btn_sell.show(); _this.btn_build.show(); }
|
|
document.body.classList.remove("is-paused");
|
|
}
|
|
};
|
|
|
|
|
|
var btnRestart = document.getElementById("ui-btn-restart");
|
|
if (btnRestart) btnRestart.onclick = function() {
|
|
setTimeout(function () {
|
|
TD.stage.clear();
|
|
TD.is_paused = true;
|
|
|
|
// Reset all Consumable inventories, cooldowns, and buffs
|
|
if (TD.cooldowns && TD.inventory) {
|
|
for (var k in TD.cooldowns) {
|
|
TD.cooldowns[k] = 0;
|
|
TD.inventory[k] = 0;
|
|
var btn = document.getElementById("btn-item-" + k);
|
|
if (btn) btn.style.backgroundImage = "none";
|
|
if (TD.updateItemButton) TD.updateItemButton(k);
|
|
}
|
|
}
|
|
// --- RESTART BUG FIX: Relock Towers ---
|
|
if (TD.factionData) {
|
|
for (var faction in TD.factionData) {
|
|
if (TD.factionData.hasOwnProperty(faction)) {
|
|
var towers = TD.factionData[faction];
|
|
for (var i = 0; i < towers.length; i++) {
|
|
// Towers 1-4 (index 0-3) are unlocked by default. 5-11 are locked.
|
|
towers[i].is_unlocked = (i < 4);
|
|
towers[i].free_first_build = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (TD.current_faction) TD.loadFaction(TD.current_faction);
|
|
// --------------------------------------
|
|
|
|
if (typeof TD.psycho_active !== 'undefined') TD.psycho_active = 0;
|
|
document.getElementById("td-board").style.boxShadow = "none";
|
|
|
|
// Hide the Game Over text and Static Overlay
|
|
var goText = document.getElementById("game-over-text");
|
|
if (goText) goText.style.display = "none";
|
|
|
|
window.triggerPanelIgnition();
|
|
TD.start();
|
|
// --- FIX: Re-apply Global Starting Caps Doubler ---
|
|
if (typeof TD.money !== "undefined") {
|
|
TD.money = Math.floor(TD.money * 2.0);
|
|
}
|
|
|
|
TD.mouseHand(false);
|
|
}, 0);
|
|
};
|
|
|
|
|
|
var btnExit = document.getElementById("ui-btn-exit");
|
|
if (btnExit) btnExit.onclick = function() {
|
|
// Instantly reloads the page context to the Main Menu.
|
|
// This guarantees the engine cache is completely flushed, so when
|
|
// you pick a new Faction and hit Play Game, it loads perfectly.
|
|
window.location.reload();
|
|
};
|
|
|
|
|
|
|
|
var btnUpgrade = document.getElementById("ui-btn-upgrade");
|
|
if (btnUpgrade) btnUpgrade.onclick = function() { if(_this.scene.map.selected_building) _this.scene.map.selected_building.tryToUpgrade(_this.btn_upgrade); };
|
|
|
|
var btnSell = document.getElementById("ui-btn-sell");
|
|
if (btnSell) btnSell.onclick = function() { if(_this.scene.map.selected_building) _this.scene.map.selected_building.tryToSell(_this.btn_sell); };
|
|
|
|
var btnBuild = document.getElementById("ui-btn-build");
|
|
if (btnBuild) btnBuild.onclick = function() { if(_this.scene.map.selected_building) _this.scene.map.selected_building.tryToUpgrade(_this.btn_build); };
|
|
|
|
// Wire up the new HTML tower buying buttons and update text feedback safely
|
|
var setDesc = function(txt) { var d = document.getElementById("ui-action-desc"); if(d) d.innerText = txt; };
|
|
|
|
// Wire up the Time Controls
|
|
var btnStartWave = document.getElementById("ui-btn-start-wave");
|
|
if (btnStartWave) btnStartWave.onclick = function() {
|
|
if (TD.stage.wait_new_wave > 0) {
|
|
var secondsSkipped = Math.floor(TD.stage.wait_new_wave / TD.exp_fps);
|
|
|
|
// --- NEW: Guaranteed Minimum Wave Income ---
|
|
// Base 25 caps, plus 15 extra caps for every wave you have survived
|
|
var currentWaveIdx = (_this.scene && _this.scene.wave) ? _this.scene.wave : 0;
|
|
var guaranteedCaps = 25 + (currentWaveIdx * 15);
|
|
|
|
var bonusCaps = (secondsSkipped * 5) + guaranteedCaps; // Combine them
|
|
|
|
TD.stage.wait_new_wave = 0;
|
|
|
|
if (bonusCaps > 0 && _this.map) {
|
|
setDesc("WAVE DEPLOYED: +" + bonusCaps + " CAPS SECURED");
|
|
|
|
|
|
var btnRect = btnStartWave.getBoundingClientRect();
|
|
var targetEl = document.getElementById("ui-money-container");
|
|
var targetRect = targetEl ? targetEl.getBoundingClientRect() : btnRect;
|
|
|
|
var coin_count = Math.min(secondsSkipped, 5);
|
|
var capsPerCoin = Math.floor(bonusCaps / coin_count);
|
|
var remainder = bonusCaps % coin_count;
|
|
|
|
for (var i = 0; i < coin_count; i++) {
|
|
(function(delay, i_idx) {
|
|
setTimeout(function() {
|
|
var animImg = document.createElement("img");
|
|
animImg.src = "assets/images/caps.png";
|
|
animImg.style.position = "fixed";
|
|
|
|
var startX = btnRect.left + (btnRect.width / 2) - 20 + (Math.random() * 40 - 20);
|
|
var startY = btnRect.top - 20 + (Math.random() * 20 - 10);
|
|
|
|
animImg.style.left = startX + "px";
|
|
animImg.style.top = startY + "px";
|
|
animImg.style.width = "40px";
|
|
animImg.style.height = "40px";
|
|
animImg.style.zIndex = "999999";
|
|
animImg.style.pointerEvents = "none";
|
|
animImg.style.transition = "all 1.2s ease-in-out";
|
|
animImg.style.filter = "drop-shadow(0px 0px 10px rgba(255, 255, 255, 0.7))";
|
|
document.body.appendChild(animImg);
|
|
|
|
void animImg.offsetWidth;
|
|
animImg.style.left = (targetRect.left + (targetRect.width / 2) - 20) + "px";
|
|
animImg.style.top = (targetRect.top + (targetRect.height / 2) - 20) + "px";
|
|
animImg.style.transform = "scale(0.5) rotate(360deg)";
|
|
animImg.style.opacity = "0.2";
|
|
|
|
// Exactly when the 1.2s animation finishes, delete the image AND award the caps!
|
|
setTimeout(function() {
|
|
if (animImg.parentNode) animImg.parentNode.removeChild(animImg);
|
|
TD.money += capsPerCoin + (i_idx === 0 ? remainder : 0);
|
|
}, 1200);
|
|
|
|
}, delay * 100);
|
|
})(i, i);
|
|
}
|
|
} else {
|
|
setDesc("INITIATING EARLY WAVE...");
|
|
}
|
|
|
|
}
|
|
};
|
|
|
|
|
|
// NEW: Helper to safely update global speed and recalculate tower fire rates
|
|
var changeGameSpeed = function(newSpeed) {
|
|
TD.global_speed = newSpeed;
|
|
if (_this.map && _this.map.buildings) {
|
|
TD.lang.each(_this.map.buildings, function(b) {
|
|
b._fire_wait2 = Math.floor(Math.max(TD.exp_fps / (b.speed * (TD.global_speed / 0.2)), 1));
|
|
});
|
|
}
|
|
|
|
|
|
|
|
var multiplier = Math.round(TD.global_speed / 0.2);
|
|
|
|
// Inject the blinking animation globally if it doesn't exist
|
|
if (!document.getElementById("blink-style")) {
|
|
var style = document.createElement("style");
|
|
style.id = "blink-style";
|
|
style.innerHTML = "@keyframes blinker { 50% { opacity: 0.2; } } .blink-text { animation: blinker 1.5s linear infinite; color: #ffcc00; }";
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
var speedReadout = document.getElementById("ui-speed-readout");
|
|
var speedVal = document.getElementById("ui-speed-val");
|
|
var btnSpeed = document.getElementById("ui-btn-speed");
|
|
|
|
if (btnSpeed) btnSpeed.innerHTML = "SPEED UP"; // Keep button static
|
|
|
|
if (speedReadout && speedVal) {
|
|
if (multiplier > 1) {
|
|
speedVal.innerHTML = ">> " + multiplier + "X";
|
|
speedReadout.style.display = "block";
|
|
} else {
|
|
speedReadout.style.display = "none";
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
var btnSlow = document.getElementById("ui-btn-slow");
|
|
|
|
if (btnSlow) btnSlow.onclick = function() {
|
|
var newSpeed = Math.round((TD.global_speed - 0.2) * 10) / 10;
|
|
if (newSpeed >= 0.2) {
|
|
changeGameSpeed(newSpeed);
|
|
setDesc("TIME DILATION: DECREASED TO " + Math.round(TD.global_speed / 0.2) + "X");
|
|
}
|
|
};
|
|
|
|
var btnSpeed = document.getElementById("ui-btn-speed");
|
|
if (btnSpeed) btnSpeed.onclick = function() {
|
|
var newSpeed = Math.round((TD.global_speed + 0.2) * 10) / 10;
|
|
if (newSpeed <= 1.0) { // Caps the maximum speed at 5x
|
|
changeGameSpeed(newSpeed);
|
|
setDesc("TIME DILATION: ACCELERATED TO " + Math.round(TD.global_speed / 0.2) + "X");
|
|
}
|
|
};
|
|
|
|
// Wire up Audio Toggles
|
|
var btnSound = document.getElementById("ui-btn-sound");
|
|
if (btnSound) {
|
|
btnSound.onclick = function() {
|
|
TD.sound_enabled = !TD.sound_enabled;
|
|
if (TD.sound_enabled) {
|
|
this.classList.remove("unlit");
|
|
this.style.removeProperty("cursor"); // Restore default hover cursor
|
|
this.className = this.className.replace(/warm-up-[\w-]+/g, "").trim(); // Strip animations so it instantly lights up solid
|
|
setDesc("AUDIO: SOUND EFFECTS ENABLED");
|
|
} else {
|
|
this.classList.add("unlit");
|
|
this.style.setProperty("cursor", "pointer", "important"); // Keep clickable cursor
|
|
setDesc("AUDIO: SOUND EFFECTS MUTED");
|
|
}
|
|
};
|
|
}
|
|
|
|
var btnMusic = document.getElementById("ui-btn-music");
|
|
if (btnMusic) {
|
|
btnMusic.onclick = function() {
|
|
TD.music_enabled = !TD.music_enabled;
|
|
if (TD.music_enabled) {
|
|
this.classList.remove("unlit");
|
|
this.style.removeProperty("cursor");
|
|
this.className = this.className.replace(/warm-up-[\w-]+/g, "").trim(); // Strip animations so it instantly lights up solid
|
|
setDesc("AUDIO: MUSIC ENABLED");
|
|
TD.playMusic("bgm_game"); // Start music
|
|
} else {
|
|
this.classList.add("unlit");
|
|
this.style.setProperty("cursor", "pointer", "important");
|
|
setDesc("AUDIO: MUSIC MUTED");
|
|
TD.stopMusic(); // Stop music
|
|
}
|
|
};
|
|
}
|
|
|
|
var btnSave = document.getElementById("ui-btn-save");
|
|
if (btnSave) {
|
|
btnSave.onclick = function() {
|
|
// Gather critical game state
|
|
var saveData = {
|
|
money: TD.money,
|
|
score: TD.score,
|
|
wave: _this.scene ? _this.scene.wave : 0,
|
|
faction: TD.current_faction,
|
|
difficulty: TD.difficulty,
|
|
specials: window.selectedSPECIALs || [],
|
|
towers_unlocked: []
|
|
};
|
|
|
|
// Capture which towers the player has bought access to
|
|
if (TD.factionData && TD.current_faction) {
|
|
var towers = TD.factionData[TD.current_faction];
|
|
for (var i = 0; i < towers.length; i++) {
|
|
saveData.towers_unlocked.push(towers[i].is_unlocked);
|
|
}
|
|
}
|
|
|
|
// Create file and force download
|
|
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(saveData));
|
|
var downloadAnchorNode = document.createElement('a');
|
|
downloadAnchorNode.setAttribute("href", dataStr);
|
|
downloadAnchorNode.setAttribute("download", "wasteland_save.json");
|
|
document.body.appendChild(downloadAnchorNode); // required for Firefox
|
|
downloadAnchorNode.click();
|
|
downloadAnchorNode.remove();
|
|
|
|
setDesc("SYSTEM STATE SAVED TO WASTELAND_SAVE.JSON");
|
|
};
|
|
}
|
|
|
|
// Wire up Consumables
|
|
|
|
|
|
|
|
// Wire up Consumables
|
|
var bStim = document.getElementById("btn-item-stimpak");
|
|
|
|
if (bStim) bStim.onclick = function() {
|
|
// Prevent use if already at 100 HP
|
|
if (TD.inventory.stimpak > 0 && TD.cooldowns.stimpak <= 0 && TD.life < 100) {
|
|
TD.inventory.stimpak--;
|
|
// Only trigger cooldown if they have more left
|
|
TD.cooldowns.stimpak = (TD.inventory.stimpak > 0) ? TD.getCooldownFrames() : 0;
|
|
|
|
var healAmount = 15;
|
|
if (TD.life + healAmount > 100) healAmount = 100 - TD.life;
|
|
TD.life += healAmount;
|
|
|
|
setDesc("STIMPAK ADMINISTERED: +" + healAmount + " HP");
|
|
if (window.IntelData && TD.Announcer) TD.Announcer.add(window.IntelData.getEvent("consumable_used", TD.current_faction), 4);
|
|
TD.updateItemButton("stimpak");
|
|
|
|
} else if (TD.life >= 100) {
|
|
setDesc("HP FULL. STIMPAK NOT NEEDED.");
|
|
}
|
|
};
|
|
|
|
var bPsycho = document.getElementById("btn-item-psycho");
|
|
if (bPsycho) bPsycho.onclick = function() {
|
|
if (TD.inventory.psycho > 0 && TD.cooldowns.psycho <= 0) {
|
|
TD.inventory.psycho--;
|
|
TD.cooldowns.psycho = (TD.inventory.psycho > 0) ? TD.getCooldownFrames() : 0;
|
|
|
|
TD.psycho_active = 15 * 24; // Extended to 15 seconds
|
|
|
|
document.getElementById("td-board").style.boxShadow = "inset 0 0 50px rgba(255,0,0,0.5)";
|
|
|
|
setDesc("PSYCHO INJECTED! +25% FIRE RATE AND DAMAGE!");
|
|
if (window.IntelData && TD.Announcer) TD.Announcer.add(window.IntelData.getEvent("consumable_used", TD.current_faction), 4);
|
|
TD.updateItemButton("psycho");
|
|
|
|
}
|
|
};
|
|
|
|
|
|
// Wire up Cryo Mine
|
|
var bCryo = document.getElementById("btn-item-cryomine");
|
|
if (bCryo) bCryo.onclick = function() {
|
|
if (TD.inventory.cryomine > 0 && TD.cooldowns.cryomine <= 0 && _this.map) {
|
|
_this.map.cancelPreBuild(); // Clear ghost FIRST
|
|
TD.mode = "deploy_cryomine"; // THEN set mode
|
|
setDesc("DEPLOYING CRYO MINE. Click map to place.");
|
|
TD.mouseHand(true);
|
|
}
|
|
};
|
|
|
|
// Wire up Cap Mine
|
|
var bCap = document.getElementById("btn-item-capmine");
|
|
if (bCap) bCap.onclick = function() {
|
|
if (TD.inventory.capmine > 0 && TD.cooldowns.capmine <= 0 && _this.map) {
|
|
_this.map.cancelPreBuild(); // Clear ghost FIRST
|
|
TD.mode = "deploy_capmine"; // THEN set mode
|
|
setDesc("DEPLOYING CAP MINE. Click map to place.");
|
|
TD.mouseHand(true);
|
|
}
|
|
};
|
|
|
|
// Wire up Artillery
|
|
var bArt = document.getElementById("btn-item-artillery");
|
|
if (bArt) bArt.onclick = function() {
|
|
if (TD.inventory.artillery > 0 && TD.cooldowns.artillery <= 0 && _this.map) {
|
|
_this.map.cancelPreBuild(); // Clear ghost FIRST
|
|
TD.mode = "deploy_artillery"; // THEN set mode
|
|
setDesc("CALLING ARTILLERY STRIKE. Click map to target.");
|
|
TD.mouseHand(true);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
// Wire up the Checkbox
|
|
|
|
var chkGrid = document.getElementById("ui-chk-grid");
|
|
if (chkGrid) {
|
|
// Ensure the checkbox matches the game's internal variable
|
|
chkGrid.checked = !!TD.show_grid;
|
|
chkGrid.onchange = function() {
|
|
TD.show_grid = this.checked;
|
|
setDesc(TD.show_grid ? "VISUAL OVERLAY: GRID ONLINE" : "VISUAL OVERLAY: GRID OFFLINE");
|
|
};
|
|
}
|
|
|
|
|
|
|
|
// Wire up building buttons (11 Slots) - Build or Unlock Logic
|
|
for (var i = 1; i <= 11; i++) {
|
|
(function(index) {
|
|
var btn = document.getElementById("btn-buy-tower-" + index);
|
|
if (btn) {
|
|
btn.onclick = function() {
|
|
if (!TD.current_faction || !TD.factionData[TD.current_faction]) return;
|
|
|
|
var tData = TD.factionData[TD.current_faction][index - 1]; // Array is 0-indexed
|
|
if (!tData) return;
|
|
|
|
if (tData.is_unlocked) {
|
|
// It is unlocked, prepare to build
|
|
if (_this.map) {
|
|
var buildCost = tData.free_first_build ? 0 : tData.cost;
|
|
if (TD.money >= buildCost) {
|
|
if (tData.id === "mr_handy") {
|
|
|
|
_this.map.cancelPreBuild();
|
|
TD.mode = "deploy_handy";
|
|
setDesc("DEPLOYING MR. HANDY. Click map to place.");
|
|
TD.mouseHand(true);
|
|
} else {
|
|
_this.map.preBuild("tower_" + index);
|
|
setDesc("DEPLOYING " + tData.name + ". Click map to place.");
|
|
}
|
|
} else {
|
|
setDesc("NOT ENOUGH CAPS TO BUILD! Need " + buildCost);
|
|
}
|
|
}
|
|
} else {
|
|
// It is locked, try to unlock it using the dynamic cost
|
|
var unlockCost = tData.unlock_cost || 0;
|
|
if (TD.money >= unlockCost) {
|
|
TD.money -= unlockCost;
|
|
tData.is_unlocked = true;
|
|
tData.free_first_build = true; // NEW: Set the free build flag
|
|
TD.loadFaction(TD.current_faction); // Refresh buttons
|
|
setDesc(tData.name + " AUTHORIZED.");
|
|
if (typeof TD.logEvent === 'function') TD.logEvent("NEW TECH AUTHORIZED: " + tData.name + " DEPLOYMENT READY");
|
|
} else {
|
|
setDesc("NOT ENOUGH CAPS TO UNLOCK! Need " + unlockCost);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
})(i);
|
|
}
|
|
// SHIFT + A Listener for Admin Mode
|
|
document.onkeydown = function(e) {
|
|
if (e.shiftKey && (e.key === "A" || e.key === "a")) {
|
|
TD.admin_mode = !TD.admin_mode;
|
|
|
|
var d = document.getElementById("ui-action-desc");
|
|
var adminPanel = document.getElementById("admin-panel");
|
|
var controlPanel = document.getElementById("control-panel");
|
|
|
|
if (TD.admin_mode) {
|
|
// Safely pause and wipe action without destroying the rendering engine
|
|
if (_this.scene) {
|
|
_this.scene.pause();
|
|
var activeMap = _this.scene.map;
|
|
if (activeMap) {
|
|
// Kill all monsters and bullets
|
|
TD.lang.each(activeMap.monsters, function(m) { m.is_valid = false; });
|
|
activeMap.monsters = [];
|
|
TD.lang.each(activeMap.bullets, function(b) { b.is_valid = false; });
|
|
activeMap.bullets = [];
|
|
// Remove all towers
|
|
TD.lang.each(activeMap.buildings, function(b) { b.grid.removeBuilding(); });
|
|
activeMap.buildings = [];
|
|
activeMap.has_weapon = false;
|
|
}
|
|
_this.scene.wave = 0; // Reset wave counter
|
|
}
|
|
|
|
// Hide pre-building ghost tower
|
|
if (_this.scene && _this.scene.map && _this.scene.map.pre_building) _this.scene.map.pre_building.hide();
|
|
|
|
if (controlPanel) controlPanel.style.display = "none";
|
|
if (adminPanel) adminPanel.style.display = "flex";
|
|
if(d) d.innerText = "G.E.C.K. EDITOR OVERRIDE ENGAGED";
|
|
} else {
|
|
// RESUME GAMEPLAY
|
|
if (_this.scene) {
|
|
_this.scene.start(); // Restart the internal scene clock
|
|
TD.stage.wait_new_wave = TD.exp_fps * 3; // Reset the wave countdown timer
|
|
}
|
|
|
|
if (adminPanel) adminPanel.style.display = "none";
|
|
if (controlPanel) controlPanel.style.display = "flex";
|
|
if(d) d.innerText = "AWAITING PIP-BOY INPUT...";
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
// Wire up the Admin Brushes
|
|
var setBrush = function(brushName) { TD.admin_brush = brushName; var d = document.getElementById("ui-action-desc"); if(d) d.innerText = "BRUSH SELECTED: " + brushName.toUpperCase(); };
|
|
var bPath = document.getElementById("admin-brush-path"); if(bPath) bPath.onclick = function() { setBrush("path"); };
|
|
var bBlock = document.getElementById("admin-brush-block"); if(bBlock) bBlock.onclick = function() { setBrush("block"); };
|
|
var bClear = document.getElementById("admin-brush-clear"); if(bClear) bClear.onclick = function() { setBrush("clear"); };
|
|
var bEnt = document.getElementById("admin-brush-ent"); if(bEnt) bEnt.onclick = function() { setBrush("entrance"); };
|
|
var bExit = document.getElementById("admin-brush-exit"); if(bExit) bExit.onclick = function() { setBrush("exit"); };
|
|
|
|
// NEW: Tower Brush Binding
|
|
var bTower = document.getElementById("admin-brush-tower");
|
|
if(bTower) bTower.onclick = function() { setBrush("tower"); };
|
|
|
|
// New Tools: Fill, Clear All, and Overlay Toggle
|
|
|
|
var bFill = document.getElementById("admin-btn-fill");
|
|
if(bFill) bFill.onclick = function() {
|
|
TD.lang.each(_this.map.grids, function(g) {
|
|
if (TD.admin_brush == "path") { g.passable_flag = 1; g.build_flag = 0; }
|
|
else if (TD.admin_brush == "block") { g.passable_flag = 0; g.build_flag = 0; }
|
|
else if (TD.admin_brush == "clear") { g.passable_flag = 1; g.build_flag = 1; }
|
|
});
|
|
};
|
|
var bClearAll = document.getElementById("admin-btn-clearall");
|
|
if(bClearAll) bClearAll.onclick = function() {
|
|
// Reset everything to default Clear state
|
|
TD.lang.each(_this.map.grids, function(g) { g.passable_flag = 1; g.build_flag = 1; g.is_entrance = false; g.is_exit = false; });
|
|
|
|
// CRITICAL FIX: Completely wipe the memory arrays and visual assets
|
|
_this.map.entrances = [];
|
|
_this.map.exits = [];
|
|
_this.map.entrance = null;
|
|
_this.map.exit = null;
|
|
_this.map.assets = [];
|
|
|
|
// Force an immediate redraw so the screen actually clears!
|
|
_this.scene.render();
|
|
};
|
|
|
|
|
|
|
|
TD.admin_overlay = true; // Overlay is on by default
|
|
var bToggle = document.getElementById("admin-btn-toggle");
|
|
if(bToggle) bToggle.onclick = function() { TD.admin_overlay = !TD.admin_overlay; };
|
|
|
|
// NEW: Open Asset Library Modal
|
|
var btnOpenAssets = document.getElementById("admin-btn-open-assets");
|
|
if (btnOpenAssets) {
|
|
btnOpenAssets.onclick = function() {
|
|
if (TD.populateAssetModal) TD.populateAssetModal();
|
|
document.getElementById("asset-modal-overlay").style.display = "flex";
|
|
};
|
|
}
|
|
|
|
// Wire up the Map Loader
|
|
|
|
var btnLoadMap = document.getElementById("admin-btn-loadmap");
|
|
if (btnLoadMap) btnLoadMap.onclick = function() {
|
|
var lvl = document.getElementById("admin-level-select").value;
|
|
var cfg = TD.getDefaultStageData(lvl);
|
|
if (cfg && cfg.map && _this.scene && _this.scene.map) {
|
|
|
|
// --- NEW: Load Background Image and Update Text Box ---
|
|
if (cfg.bg_image) {
|
|
var board = document.getElementById("td-board");
|
|
if (board) {
|
|
board.style.backgroundImage = "url('" + cfg.bg_image + "')";
|
|
board.style.backgroundSize = "100% 100%";
|
|
board.style.backgroundRepeat = "no-repeat";
|
|
}
|
|
var imgInput = document.getElementById("admin-img-input");
|
|
if (imgInput) imgInput.value = cfg.bg_image;
|
|
}
|
|
|
|
// --- NEW: Load Environment Effect ---
|
|
var envOverlay = document.getElementById("env-effect-overlay");
|
|
if (envOverlay) {
|
|
if (cfg.map && cfg.map.env_effect) {
|
|
var eCfg = cfg.map.env_effect;
|
|
document.getElementById("admin-env-src").value = eCfg.src;
|
|
document.getElementById("admin-env-type").value = eCfg.type;
|
|
document.getElementById("admin-env-opacity").value = eCfg.opacity;
|
|
document.getElementById("admin-env-speed").value = eCfg.speed;
|
|
|
|
envOverlay.style.backgroundImage = "url('" + eCfg.src + "')";
|
|
document.documentElement.style.setProperty("--env-opacity", eCfg.opacity);
|
|
envOverlay.style.animation = "none";
|
|
void envOverlay.offsetWidth;
|
|
if (eCfg.type === "static") { envOverlay.style.opacity = eCfg.opacity; }
|
|
else if (eCfg.type === "fade") { envOverlay.style.opacity = 0; envOverlay.style.animation = "env-fade " + eCfg.speed + "s infinite ease-in-out"; }
|
|
else { envOverlay.style.opacity = eCfg.opacity; envOverlay.style.animation = "env-" + eCfg.type.replace("_", "-") + " " + eCfg.speed + "s infinite linear"; }
|
|
envOverlay.style.display = "block";
|
|
} else {
|
|
// Clear inputs and hide overlay if no effect is saved
|
|
document.getElementById("admin-env-src").value = "";
|
|
envOverlay.style.display = "none";
|
|
envOverlay.style.animation = "none";
|
|
}
|
|
}
|
|
// ----------------------------------------------------
|
|
|
|
var m = _this.scene.map;
|
|
// Reset grid to default blank slate
|
|
TD.lang.each(m.grids, function(g) { g.passable_flag = 1; g.build_flag = 1; g.is_entrance = false; g.is_exit = false; });
|
|
|
|
// CRITICAL FIX: Completely wipe the memory arrays before loading new data
|
|
m.entrances = []; m.exits = []; m.entrance = null; m.exit = null;
|
|
m.assets = []; // --- FIX: Wipe old visual paths and assets! ---
|
|
|
|
|
|
|
|
// Reapply config
|
|
if (cfg.map.grids_cfg) {
|
|
TD.lang.each(cfg.map.grids_cfg, function (obj) {
|
|
var grid = m.getGrid(obj.pos[0], obj.pos[1]);
|
|
if (grid) {
|
|
if (!isNaN(obj.passable_flag)) grid.passable_flag = obj.passable_flag;
|
|
if (!isNaN(obj.build_flag)) grid.build_flag = obj.build_flag;
|
|
if (obj.building) {
|
|
grid.addBuilding(obj.building);
|
|
if (obj.building_level && grid.building) {
|
|
for (var lvl = 1; lvl < obj.building_level; lvl++) grid.building.upgrade();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Properly rebuild the Entrance arrays
|
|
if (cfg.map.entrance) { var eg = m.getGrid(cfg.map.entrance[0], cfg.map.entrance[1]); if(eg) { eg.is_entrance = true; eg.passable_flag = 1; eg.build_flag = 0; m.entrances.push(eg); } }
|
|
if (cfg.map.entrances) {
|
|
for (var i = 0; i < cfg.map.entrances.length; i++) {
|
|
var eg = m.getGrid(cfg.map.entrances[i][0], cfg.map.entrances[i][1]);
|
|
if (eg) { eg.is_entrance = true; eg.passable_flag = 1; eg.build_flag = 0; m.entrances.push(eg); }
|
|
}
|
|
}
|
|
|
|
// Properly rebuild the Exit arrays
|
|
if (cfg.map.exit) { var xg = m.getGrid(cfg.map.exit[0], cfg.map.exit[1]); if(xg) { xg.is_exit = true; xg.passable_flag = 1; xg.build_flag = 0; m.exits.push(xg); } }
|
|
if (cfg.map.exits) {
|
|
for (var i = 0; i < cfg.map.exits.length; i++) {
|
|
var xg = m.getGrid(cfg.map.exits[i][0], cfg.map.exits[i][1]);
|
|
if (xg) { xg.is_exit = true; xg.passable_flag = 1; xg.build_flag = 0; m.exits.push(xg); }
|
|
}
|
|
}
|
|
|
|
m.entrance = m.entrances[0] || null;
|
|
m.exit = m.exits[0] || null;
|
|
|
|
// --- NEW: Load Custom Wave Data if it exists ---
|
|
if (cfg.custom_waves) {
|
|
TD.custom_wave_data = JSON.parse(JSON.stringify(cfg.custom_waves));
|
|
} else {
|
|
// Reset to blank slate if no custom waves exist on this map
|
|
TD.custom_wave_data = {};
|
|
for (var w = 1; w <= 15; w++) {
|
|
TD.custom_wave_data[w] = {
|
|
story_enable: false,
|
|
story_data: { "ALL": {title:"", desc:""}, "NCR": {title:"", desc:""}, "BOS": {title:"", desc:""}, "MINUTEMEN": {title:"", desc:""}, "RAIDERS": {title:"", desc:""} },
|
|
towers: {}, spawns: []
|
|
};
|
|
}
|
|
}
|
|
|
|
if (typeof TD.we_loadCurrentWave === 'function' && document.getElementById('wave-editor-modal-overlay').style.display !== 'none') {
|
|
TD.we_loadCurrentWave();
|
|
}
|
|
|
|
// --- FIX: Rebuild Visual Assets for the Loaded Map ---
|
|
|
|
if (cfg.map.assets) {
|
|
TD.lang.each(cfg.map.assets, function (a) {
|
|
var imgObj = new Image();
|
|
imgObj.src = a.src;
|
|
var x_px = m.x + (a.mx * TD.grid_size);
|
|
var y_px = m.y + (a.my * TD.grid_size);
|
|
var w_px = a.grid_x * TD.grid_size;
|
|
var h_px = a.grid_y * TD.grid_size;
|
|
|
|
m.assets.push({
|
|
id: a.id, src: a.src, img: imgObj,
|
|
mx: a.mx, my: a.my, grid_x: a.grid_x, grid_y: a.grid_y,
|
|
blocks_path: a.blocks_path, category: a.category,
|
|
x: x_px, y: y_px, width: w_px, height: h_px,
|
|
y2: y_px + h_px, is_asset: true
|
|
});
|
|
|
|
for (var dx = 0; dx < a.grid_x; dx++) {
|
|
for (var dy = 0; dy < a.grid_y; dy++) {
|
|
var g = m.getGrid(a.mx + dx, a.my + dy);
|
|
if (g) {
|
|
if (a.category !== "path") g.build_flag = 0;
|
|
if (a.blocks_path) g.passable_flag = 0;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "GRID DATA LOADED: " + lvl;
|
|
}
|
|
};
|
|
|
|
|
|
// Wire up the Image Loader
|
|
var btnLoadImg = document.getElementById("admin-btn-loadimg");
|
|
if (btnLoadImg) btnLoadImg.onclick = function() {
|
|
var url = document.getElementById("admin-img-input").value;
|
|
if(url) {
|
|
var board = document.getElementById("td-board");
|
|
if (board) {
|
|
board.style.backgroundImage = "url('" + url + "')";
|
|
board.style.backgroundSize = "100% 100%";
|
|
board.style.backgroundRepeat = "no-repeat";
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "BACKGROUND LOADED: " + url;
|
|
}
|
|
}
|
|
};
|
|
|
|
// --- NEW: Wire up Environmental Effects ---
|
|
var btnEnvApply = document.getElementById("admin-btn-env-apply");
|
|
if (btnEnvApply) btnEnvApply.onclick = function() {
|
|
var envOverlay = document.getElementById("env-effect-overlay");
|
|
var src = document.getElementById("admin-env-src").value;
|
|
if (!envOverlay) return;
|
|
|
|
if (!src) {
|
|
envOverlay.style.display = "none";
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "ENVIRONMENT CLEARED.";
|
|
return;
|
|
}
|
|
|
|
var type = document.getElementById("admin-env-type").value;
|
|
var opacity = parseFloat(document.getElementById("admin-env-opacity").value) || 0.5;
|
|
var speed = parseInt(document.getElementById("admin-env-speed").value) || 20;
|
|
|
|
envOverlay.style.backgroundImage = "url('" + src + "')";
|
|
document.documentElement.style.setProperty("--env-opacity", opacity);
|
|
|
|
// Clear old animations first
|
|
envOverlay.style.animation = "none";
|
|
void envOverlay.offsetWidth; // Trigger reflow
|
|
|
|
if (type === "static") {
|
|
envOverlay.style.opacity = opacity;
|
|
} else if (type === "fade") {
|
|
envOverlay.style.opacity = 0; // The animation handles visibility
|
|
envOverlay.style.animation = "env-fade " + speed + "s infinite ease-in-out";
|
|
} else {
|
|
envOverlay.style.opacity = opacity;
|
|
var animName = "env-" + type.replace("_", "-");
|
|
envOverlay.style.animation = animName + " " + speed + "s infinite linear";
|
|
}
|
|
|
|
envOverlay.style.display = "block";
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "ENVIRONMENT EFFECT APPLIED.";
|
|
};
|
|
|
|
|
|
// --- NEW: BULLETPROOF PATH VALIDATOR ---
|
|
var btnValidate = document.getElementById("admin-btn-validate");
|
|
if (btnValidate) {
|
|
btnValidate.onclick = function() {
|
|
var m = _this.scene.map;
|
|
if (!m || !m.entrance || !m.exit) {
|
|
alert("ERROR: Missing Entrance or Exit!");
|
|
return;
|
|
}
|
|
|
|
// Native Engine Pathfinder
|
|
var fw = new TD.FindWay(
|
|
m.grid_x, m.grid_y,
|
|
m.entrance.mx, m.entrance.my,
|
|
m.exit.mx, m.exit.my,
|
|
function (x, y) { return m.checkPassable(x, y); }
|
|
);
|
|
|
|
// fw.is_arrived is true if it reached the exit.
|
|
if (fw.is_arrived) {
|
|
var validPath = [m.entrance];
|
|
if (fw.way && fw.way.length > 0) {
|
|
for (var i = 0; i < fw.way.length; i++) {
|
|
var gx = fw.way[i][0];
|
|
var gy = fw.way[i][1];
|
|
var g = m.getGrid(gx, gy);
|
|
if (g) validPath.push(g);
|
|
}
|
|
} else {
|
|
validPath.push(m.exit);
|
|
}
|
|
|
|
// 1. ALERT FIRST so you always get the message!
|
|
alert("SUCCESS! Path is connected (" + validPath.length + " tiles long).");
|
|
|
|
// 2. Safe Hologram Render (No crashing, no flashing)
|
|
if (!m._holo_render_active) {
|
|
m._holo_render_active = true;
|
|
var originalRender = m.render;
|
|
var life = 60; // Lasts ~2 seconds
|
|
|
|
m.render = function() {
|
|
originalRender.apply(this, arguments); // Draw map normally
|
|
|
|
if (life > 0) {
|
|
var ctx = TD.ctx;
|
|
ctx.fillStyle = "rgba(0, 255, 255, " + (life / 60) * 0.5 + ")";
|
|
for (var i = 0; i < validPath.length; i++) {
|
|
var g = validPath[i];
|
|
if (g) ctx.fillRect(g.x, g.y, g.width, g.height);
|
|
}
|
|
life--;
|
|
} else {
|
|
// Clean up and restore original render
|
|
m.render = originalRender;
|
|
m._holo_render_active = false;
|
|
}
|
|
};
|
|
}
|
|
|
|
} else {
|
|
alert("ERROR: The path is broken! Make sure you used the Blue Path brush to connect the Entrance to the Exit.");
|
|
}
|
|
};
|
|
}
|
|
// -------------------------------------
|
|
|
|
// --- NEW: MAP EXPORTER ---
|
|
var btnExport = document.getElementById("admin-btn-export");
|
|
if (btnExport) {
|
|
btnExport.onclick = function() {
|
|
var m = _this.scene.map;
|
|
var spawnMode = document.getElementById("admin-spawn-mode") ? document.getElementById("admin-spawn-mode").value : "random";
|
|
|
|
// 1. Get Entrances and Exits as Arrays
|
|
var entArr = [];
|
|
if (m.entrances && m.entrances.length > 0) {
|
|
for (var i = 0; i < m.entrances.length; i++) entArr.push("[" + m.entrances[i].mx + ", " + m.entrances[i].my + "]");
|
|
}
|
|
var extArr = [];
|
|
if (m.exits && m.exits.length > 0) {
|
|
for (var i = 0; i < m.exits.length; i++) extArr.push("[" + m.exits[i].mx + ", " + m.exits[i].my + "]");
|
|
}
|
|
|
|
var entStr = entArr.length > 0 ? "[" + entArr.join(", ") + "]" : "[]";
|
|
var extStr = extArr.length > 0 ? "[" + extArr.join(", ") + "]" : "[]";
|
|
|
|
// 2. Build Grids Config
|
|
var grids = [];
|
|
TD.lang.each(m.grids, function(g) {
|
|
if (g.passable_flag !== 1 || g.build_flag !== 1 || g.building) {
|
|
var gStr = "{ pos: [" + g.mx + ", " + g.my + "]";
|
|
if (g.passable_flag !== 1) gStr += ", passable_flag: " + g.passable_flag;
|
|
if (g.build_flag !== 1) gStr += ", build_flag: " + g.build_flag;
|
|
if (g.building) gStr += ", building: '" + g.building.type + "', building_level: " + g.building.level;
|
|
gStr += " }";
|
|
grids.push(gStr);
|
|
}
|
|
});
|
|
|
|
// 2.5 Build Assets Config (Fully dynamic for future assets)
|
|
var assetArr = [];
|
|
if (m.assets && m.assets.length > 0) {
|
|
TD.lang.each(m.assets, function(a) {
|
|
// Rebuild the core properties needed to recreate this asset on load
|
|
var aStr = "{ id: '" + a.id + "', src: '" + a.src + "', mx: " + a.mx + ", my: " + a.my + ", grid_x: " + a.grid_x + ", grid_y: " + a.grid_y + ", blocks_path: " + a.blocks_path + ", category: '" + a.category + "' }";
|
|
assetArr.push(aStr);
|
|
});
|
|
}
|
|
var assetStr = assetArr.length > 0 ? "assets: [" + assetArr.join(", ") + "], " : "";
|
|
|
|
// --- NEW: Build Environment Config ---
|
|
var envStr = "";
|
|
var eSrc = document.getElementById("admin-env-src").value;
|
|
if (eSrc) {
|
|
var eType = document.getElementById("admin-env-type").value;
|
|
var eOp = document.getElementById("admin-env-opacity").value;
|
|
var eSpd = document.getElementById("admin-env-speed").value;
|
|
envStr = "env_effect: { src: '" + eSrc + "', type: '" + eType + "', opacity: " + eOp + ", speed: " + eSpd + " }, ";
|
|
}
|
|
|
|
// 3. Form the final string (SINGLE LINE, DYNAMIC NAME)
|
|
|
|
if (typeof TD.we_saveCurrentWave === 'function') TD.we_saveCurrentWave(); // Ensure latest edits are saved
|
|
|
|
var waveDataStr = "";
|
|
if (typeof TD.custom_wave_data !== 'undefined') {
|
|
waveDataStr = "custom_waves: " + JSON.stringify(TD.custom_wave_data) + ", ";
|
|
}
|
|
|
|
var bgImg = document.getElementById("admin-img-input").value || "map01.jpg";
|
|
var loadedMapName = document.getElementById("admin-level-select") ? document.getElementById("admin-level-select").value : "custom_map";
|
|
|
|
var finalCode = loadedMapName + ": { " + waveDataStr + "bg_image: '" + bgImg + "', map: { " + envStr + "map_type: '" + m.map_type + "', spawn_mode: '" + spawnMode + "', grid_x: " + m.grid_x + ", grid_y: " + m.grid_y + ", x: " + m.x + ", y: " + m.y + ", entrances: " + entStr + ", exits: " + extStr + ", " + assetStr + "grids_cfg: [" + grids.join(", ") + "] }, panel: { x: 1920, y: TD.padding, map: { grid_x: 3, grid_y: 3, x: 0, y: 110 * _TD.retina, grids_cfg: [] } }, config: config_base, newWave: newWave_func },";
|
|
|
|
var outBox = document.getElementById("admin-export-out");
|
|
if (outBox) {
|
|
outBox.value = finalCode;
|
|
outBox.select();
|
|
}
|
|
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "CODE GENERATED! Click COPY CODE.";
|
|
};
|
|
}
|
|
|
|
// COPY BUTTON LOGIC
|
|
var btnCopy = document.getElementById("admin-btn-copy");
|
|
if (btnCopy) {
|
|
btnCopy.onclick = function() {
|
|
var outBox = document.getElementById("admin-export-out");
|
|
if (outBox && outBox.value) {
|
|
outBox.select();
|
|
document.execCommand("copy"); // Copies to clipboard
|
|
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "CODE COPIED TO CLIPBOARD!";
|
|
}
|
|
};
|
|
}
|
|
|
|
},
|
|
|
|
|
|
step: function () {
|
|
if (TD.life_recover) { this._life_recover = this._life_recover2 = TD.life_recover; this._life_recover_wait = this._life_recover_wait2 = TD.exp_fps * 3; TD.life_recover = 0; }
|
|
if (this._life_recover && (TD.iframe % TD.exp_fps_eighth == 0)) { TD.life ++; this._life_recover --; }
|
|
|
|
// --- NEW: TYPEWRITER RENDER LOOP ---
|
|
var tickerEl = document.getElementById("intel-ticker-text");
|
|
var tickerContainer = document.getElementById("intel-ticker-container");
|
|
|
|
// Assign hover pause listeners if they don't exist yet
|
|
if (tickerContainer && typeof TD.ticker_hover === 'undefined') {
|
|
TD.ticker_hover = false;
|
|
tickerContainer.onmouseover = function() { TD.ticker_hover = true; };
|
|
tickerContainer.onmouseout = function() { TD.ticker_hover = false; };
|
|
}
|
|
|
|
if (tickerEl && tickerContainer && this.scene && this.scene.state === 1) {
|
|
|
|
// Call the announcer brain every frame, it manages its own reading/typing delays now!
|
|
if (TD.Announcer) TD.Announcer.update();
|
|
|
|
// Only type if we haven't finished the string, AND the mouse isn't hovering
|
|
if (TD.typewriter_current.length < TD.typewriter_target.length && !TD.ticker_hover) {
|
|
// Slower, classic typing speed (1 character per frame at normal speed)
|
|
var charsToAdd = Math.max(1, Math.floor(1.5 * TD.global_speed * (60 / TD.exp_fps)));
|
|
|
|
TD.typewriter_current = TD.typewriter_target.substring(0, TD.typewriter_current.length + charsToAdd);
|
|
|
|
// FIX: Standard pipe cursor
|
|
tickerEl.innerHTML = TD.typewriter_current + "<span class='blink-text' style='color:#0f0; font-weight:bold;'>|</span>";
|
|
}
|
|
// Once finished, remove the cursor and tick down the reading pause
|
|
else if (TD.typewriter_current.length >= TD.typewriter_target.length && TD.typewriter_target.length > 0) {
|
|
tickerEl.innerHTML = TD.typewriter_current;
|
|
|
|
// Count down the reading pause
|
|
if (!TD.ticker_hover && TD.typewriter_pause > 0) {
|
|
TD.typewriter_pause--;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// --- NEW: V.A.T.S. UI & Timer Logic ---
|
|
|
|
|
|
|
|
var vatsBtn = document.getElementById("btn-vats-activate");
|
|
if (vatsBtn && typeof TD.vats_charge !== 'undefined') {
|
|
|
|
// --- CRITICAL FIX: Calculate the visual percentage ---
|
|
var vatsPercent = Math.min(100, Math.floor((TD.vats_charge / TD.vats_max_charge) * 100));
|
|
|
|
if (TD.vats_active) {
|
|
TD.vats_timer--;
|
|
var secsLeft = Math.ceil(TD.vats_timer / TD.exp_fps);
|
|
vatsBtn.innerHTML = "SHOTS: " + TD.vats_shots + "<br>" + secsLeft + "s";
|
|
vatsBtn.style.lineHeight = "1.2";
|
|
vatsBtn.style.backgroundImage = "none";
|
|
vatsBtn.classList.add("vats-charged-glow");
|
|
|
|
var hasValidMonsters = false;
|
|
var incomingMonsters = false;
|
|
if (this.map) {
|
|
if (this.map.monsters) {
|
|
for (var i = 0; i < this.map.monsters.length; i++) {
|
|
if (this.map.monsters[i].is_valid) { hasValidMonsters = true; break; }
|
|
}
|
|
}
|
|
incomingMonsters = (this.map._wait_add_monsters > 0 || this.map._wait_add_monsters_arr.length > 0);
|
|
}
|
|
|
|
if (TD.vats_timer <= 0 || TD.vats_shots <= 0 || (!hasValidMonsters && !incomingMonsters)) {
|
|
TD.endVats();
|
|
}
|
|
|
|
} else if (vatsPercent >= 100) {
|
|
vatsBtn.disabled = TD.is_paused; // Block activation if paused!
|
|
vatsBtn.innerHTML = "ACTIVATE<br>V.A.T.S.";
|
|
vatsBtn.style.lineHeight = "1.2";
|
|
vatsBtn.style.cursor = TD.is_paused ? "not-allowed" : "pointer";
|
|
vatsBtn.style.backgroundImage = "none";
|
|
vatsBtn.classList.add("vats-charged-glow");
|
|
|
|
} else {
|
|
vatsBtn.disabled = true;
|
|
vatsBtn.innerText = "V.A.T.S. " + vatsPercent + "%";
|
|
vatsBtn.style.cursor = "not-allowed";
|
|
vatsBtn.style.lineHeight = "normal";
|
|
vatsBtn.style.backgroundImage = "none";
|
|
|
|
// Reset everything completely so inline styles stop interfering
|
|
vatsBtn.style.removeProperty("color");
|
|
vatsBtn.style.removeProperty("box-shadow");
|
|
vatsBtn.style.removeProperty("background-color");
|
|
vatsBtn.classList.remove("vats-charged-glow");
|
|
}
|
|
|
|
}
|
|
},
|
|
|
|
render: function () {
|
|
// True Physical Nixie Tube Simulator
|
|
var updateNixie = function(containerId, targetValue, length, suffix) {
|
|
var container = document.getElementById(containerId);
|
|
if (!container) return;
|
|
|
|
var targetStr = targetValue.toString();
|
|
// Pad with spaces instead of zeros so no wire receives power
|
|
while (targetStr.length < length) targetStr = " " + targetStr;
|
|
if (suffix) targetStr += suffix;
|
|
|
|
|
|
// 1. Initialization: Build physical 0-9 wire stack
|
|
if (container.children.length === 0) {
|
|
container.innerHTML = "";
|
|
for (var i = 0; i < targetStr.length; i++) {
|
|
// Generate a random flicker profile (1 through 5)
|
|
var randomFlicker = "warm-up-" + (Math.floor(Math.random() * 5) + 1);
|
|
|
|
if (targetStr[i] === "%") {
|
|
var tube = document.createElement("div");
|
|
tube.className = "nixie-tube";
|
|
tube.setAttribute("data-current", "%");
|
|
var wire = document.createElement("div");
|
|
wire.className = "nixie-wire active " + randomFlicker;
|
|
wire.setAttribute("data-digit", "%");
|
|
wire.innerText = "%";
|
|
tube.appendChild(wire);
|
|
container.appendChild(tube);
|
|
} else {
|
|
var tube = document.createElement("div");
|
|
tube.className = "nixie-tube";
|
|
tube.setAttribute("data-current", targetStr[i]);
|
|
|
|
// Create the 10 physical wires in depth order
|
|
var wireOrder = ['0','1','2','3','4','5','6','7','8','9'];
|
|
for (var w = 0; w < wireOrder.length; w++) {
|
|
var wire = document.createElement("div");
|
|
wire.className = "nixie-wire";
|
|
wire.setAttribute("data-digit", wireOrder[w]);
|
|
wire.innerText = wireOrder[w];
|
|
if (wireOrder[w] === targetStr[i]) {
|
|
wire.classList.add("active");
|
|
wire.classList.add(randomFlicker); // Apply random flicker to the active digit
|
|
}
|
|
tube.appendChild(wire);
|
|
}
|
|
container.appendChild(tube);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 2. Trigger Ignition on changed tubes
|
|
for (var i = 0; i < targetStr.length; i++) {
|
|
if (targetStr[i] === "%") continue;
|
|
|
|
var tube = container.children[i];
|
|
if (!tube) continue;
|
|
|
|
var currentVal = tube.getAttribute("data-current");
|
|
var newTarget = targetStr[i];
|
|
|
|
if (currentVal !== newTarget) {
|
|
tube.setAttribute("data-current", newTarget);
|
|
|
|
// Find wires to turn off and on
|
|
var wires = tube.children;
|
|
for (var w = 0; w < wires.length; w++) {
|
|
var wire = wires[w];
|
|
if (wire.getAttribute("data-digit") === newTarget) {
|
|
wire.classList.add("active");
|
|
// Ignite stagger
|
|
(function(d) {
|
|
d.classList.remove("igniting");
|
|
void d.offsetWidth;
|
|
setTimeout(function() {
|
|
d.classList.add("igniting");
|
|
}, Math.random() * 80);
|
|
})(wire);
|
|
} else {
|
|
wire.classList.remove("active");
|
|
wire.classList.remove("igniting");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Caps and Score get 6 digits
|
|
updateNixie("ui-money-container", TD.money, 6, "");
|
|
updateNixie("ui-score-container", TD.score, 6, "");
|
|
|
|
// Wave gets 3 digits
|
|
updateNixie("ui-wave-container", this.scene.wave, 3, "");
|
|
|
|
// Health gets 3 or 4 digits, plus a dynamic suffix
|
|
var elLabel = document.getElementById("ui-health-label");
|
|
if (elLabel) {
|
|
if (this.map && this.map.map_type === "siege") {
|
|
elLabel.innerText = "BASE HP";
|
|
updateNixie("ui-life-container", TD.base_health, 4, "");
|
|
} else {
|
|
elLabel.innerText = "HEALTH";
|
|
updateNixie("ui-life-container", TD.life, 3, "%");
|
|
}
|
|
|
|
// Color critical health safely (causes a deep red alarm pulse)
|
|
var lifeContainer = document.getElementById("ui-life-container");
|
|
var isCritical = (this.map && this.map.map_type === "siege") ? (TD.base_health <= 300) : (TD.life <= 25);
|
|
if (lifeContainer) {
|
|
for (var i=0; i<lifeContainer.children.length; i++) {
|
|
var child = lifeContainer.children[i];
|
|
if (child && child.classList) { // SAFETY CHECK
|
|
if (isCritical) child.classList.add("critical");
|
|
else child.classList.remove("critical");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- NEW: HEALTH-BASED POWER FAILURE ---
|
|
var hpPercent = 100;
|
|
if (this.map && this.map.map_type === "siege") hpPercent = (TD.base_health / 1000) * 100;
|
|
else hpPercent = TD.life;
|
|
|
|
// --- NEW: SCREEN CRACK OVERLAYS ---
|
|
var d70 = document.getElementById("damage-overlay-70");
|
|
var d50 = document.getElementById("damage-overlay-50");
|
|
var d30 = document.getElementById("damage-overlay-30");
|
|
var d10 = document.getElementById("damage-overlay-10");
|
|
|
|
var placeCrack = function(el, topMin, topMax, leftMin, leftMax, isActive) {
|
|
if (isActive) {
|
|
// Turn ON: Instant snap
|
|
el.style.transition = "none";
|
|
|
|
if (el.style.opacity === "0" || !el.style.opacity) {
|
|
el.style.top = Math.floor(Math.random() * (topMax - topMin + 1) + topMin) + "%";
|
|
el.style.left = Math.floor(Math.random() * (leftMax - leftMin + 1) + leftMin) + "%";
|
|
el.style.transform = "rotate(" + Math.floor(Math.random() * 360) + "deg)";
|
|
}
|
|
el.style.opacity = "0.9";
|
|
} else {
|
|
// Turn OFF: Smooth fade
|
|
el.style.transition = "opacity 2s ease-out";
|
|
el.style.opacity = "0";
|
|
}
|
|
};
|
|
|
|
if (d70 && d50 && d30 && d10) {
|
|
placeCrack(d70, 5, 25, 5, 25, (hpPercent <= 70 && hpPercent > 0));
|
|
placeCrack(d50, 45, 65, 55, 75, (hpPercent <= 50 && hpPercent > 0));
|
|
placeCrack(d30, 45, 65, 5, 25, (hpPercent <= 30 && hpPercent > 0));
|
|
placeCrack(d10, 5, 25, 55, 75, (hpPercent <= 10 && hpPercent > 0));
|
|
}
|
|
|
|
|
|
// 0% Threshold (Fade out buttons, kill signal feed)
|
|
|
|
var staticOverlay = document.getElementById("signal-lost-overlay");
|
|
if (hpPercent <= 0) {
|
|
document.body.classList.add("game-over-dead");
|
|
if (staticOverlay) staticOverlay.style.display = "block";
|
|
} else {
|
|
document.body.classList.remove("game-over-dead");
|
|
if (staticOverlay) staticOverlay.style.display = "none";
|
|
}
|
|
|
|
var powerLevel = 1.0;
|
|
var sputterChance = 0; // Out of 100 per frame
|
|
|
|
if (hpPercent <= 25) { powerLevel = 0.4; sputterChance = 10; }
|
|
else if (hpPercent <= 45) { powerLevel = 0.65; sputterChance = 5; }
|
|
else if (hpPercent <= 65) { powerLevel = 0.85; sputterChance = 2; }
|
|
|
|
|
|
// Apply global dimming to all lit buttons
|
|
document.documentElement.style.setProperty('--power-level', powerLevel);
|
|
|
|
// Randomly force a lit button to restart its ignition cycle (sputter)
|
|
if (sputterChance > 0 && Math.random() * 100 < sputterChance) {
|
|
// Find all buttons that are currently enabled and not blank
|
|
var litBtns = document.querySelectorAll(".cmd-btn:not(:disabled):not(.unlit), .sys-btn:not(:disabled):not(.unlit)");
|
|
if (litBtns.length > 0) {
|
|
var randBtn = litBtns[Math.floor(Math.random() * litBtns.length)];
|
|
randBtn.className = randBtn.className.replace(/warm-up-\d/g, "").trim();
|
|
void randBtn.offsetWidth;
|
|
randBtn.classList.add("warm-up-" + (Math.floor(Math.random() * 5) + 1));
|
|
}
|
|
}
|
|
// ----------------------------------------
|
|
|
|
var elFps = document.getElementById("ui-fps"); if(elFps) elFps.innerText = TD.fps;
|
|
|
|
|
|
// --- NEW: DYNAMIC MODAL UPGRADE AFFORDABILITY CHECK ---
|
|
var modalOverlay = document.getElementById("tower-modal-overlay");
|
|
if (modalOverlay && modalOverlay.style.display === "flex") {
|
|
[1, 2].forEach(function(pathNum) {
|
|
var btn = document.getElementById("modal-btn-path" + pathNum);
|
|
if (btn && btn.hasAttribute("data-cost")) {
|
|
var cost = parseInt(btn.getAttribute("data-cost"));
|
|
var nameEl = btn.querySelector("#path" + pathNum + "-name");
|
|
var descEl = btn.querySelector("#path" + pathNum + "-desc");
|
|
if (TD.money >= cost && btn.disabled) {
|
|
btn.disabled = false;
|
|
btn.style.backgroundColor = "#2a7a2a";
|
|
btn.style.borderColor = "#0f0";
|
|
if (nameEl) nameEl.style.color = "#000";
|
|
if (descEl) descEl.style.color = "#111";
|
|
} else if (TD.money < cost && !btn.disabled) {
|
|
btn.disabled = true;
|
|
btn.style.backgroundColor = "#444";
|
|
btn.style.borderColor = "#555";
|
|
if (nameEl) nameEl.style.color = "#888";
|
|
if (descEl) descEl.style.color = "#888";
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- NEW: DYNAMIC TOWER AFFORDABILITY CHECK ---
|
|
if (TD.current_faction && TD.factionData[TD.current_faction]) {
|
|
var towers = TD.factionData[TD.current_faction];
|
|
for (var t = 0; t < 11; t++) {
|
|
var btn = document.getElementById("btn-buy-tower-" + (t + 1));
|
|
if (!btn) continue;
|
|
|
|
if (towers[t].is_unlocked) {
|
|
// Clean up flashing if any
|
|
btn.classList.remove('vats-charged-glow');
|
|
btn.style.removeProperty("box-shadow");
|
|
btn.style.removeProperty("background-color");
|
|
btn.style.removeProperty("color");
|
|
|
|
var cost = towers[t].free_first_build ? 0 : towers[t].cost; // FIX: Respect the free flag
|
|
var wasDisabled = btn.disabled;
|
|
|
|
// FIX: Mr. Handy Max Limit Check
|
|
if (towers[t].id === "mr_handy" && this.map && this.map.mr_handy_active) {
|
|
btn.disabled = true;
|
|
btn.innerHTML = towers[t].name + " <span style='color:#f55;'>(MAX)</span>";
|
|
btn.className = btn.className.replace(/warm-up-\d/g, "").trim();
|
|
}
|
|
else if (TD.money >= cost) {
|
|
btn.disabled = false;
|
|
|
|
// If it just became affordable, flicker it on!
|
|
if (wasDisabled) {
|
|
btn.className = btn.className.replace(/warm-up-\d/g, "").trim();
|
|
void btn.offsetWidth; // Force CSS reflow
|
|
btn.classList.add("warm-up-" + (Math.floor(Math.random() * 5) + 1));
|
|
}
|
|
} else {
|
|
btn.disabled = true;
|
|
// Strip glow immediately if broke
|
|
btn.className = btn.className.replace(/warm-up-\d/g, "").trim();
|
|
}
|
|
} else {
|
|
// It is locked: Make it flash YELLOW if affordable!
|
|
var unlockCost = towers[t].unlock_cost || 0;
|
|
if (TD.money >= unlockCost) {
|
|
var newlyLit = !btn.classList.contains("vats-charged-glow");
|
|
btn.classList.remove("unlit");
|
|
btn.classList.add("vats-charged-glow");
|
|
btn.style.removeProperty("box-shadow");
|
|
btn.style.removeProperty("background-color");
|
|
btn.style.removeProperty("color");
|
|
|
|
if (newlyLit && document.getAnimations) {
|
|
var anims = document.getAnimations();
|
|
var vatsAnims = [];
|
|
for (var a = 0; a < anims.length; a++) {
|
|
if (anims[a].animationName === "vats-breathe") vatsAnims.push(anims[a]);
|
|
}
|
|
if (vatsAnims.length > 0) {
|
|
var targetTime = vatsAnims[0].currentTime;
|
|
for (var a = 1; a < vatsAnims.length; a++) {
|
|
vatsAnims[a].currentTime = targetTime;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
btn.classList.add("unlit");
|
|
btn.classList.remove("vats-charged-glow");
|
|
btn.style.removeProperty("box-shadow");
|
|
btn.style.removeProperty("background-color");
|
|
btn.style.removeProperty("color");
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
var stg = this.scene.stage || TD.stage;
|
|
|
|
var elIntel = document.getElementById("ui-intel-brief");
|
|
var stateWave = document.getElementById("intel-state-wave");
|
|
|
|
if (elIntel && stg && stateWave) {
|
|
// ACCURATE STATE DETECTION: Checks active monsters AND the spawn queues
|
|
var waveInMotion = (this.map.monsters.length > 0 || this.map._wait_add_monsters > 0 || this.map._wait_add_monsters_arr.length > 0);
|
|
var isWaiting = !waveInMotion;
|
|
var sec = Math.max(0, Math.ceil(stg.wait_new_wave / TD.exp_fps));
|
|
|
|
var nextWaveIdx = this.scene.wave + 1;
|
|
var currentWaveIdx = this.scene.wave;
|
|
|
|
if (isWaiting) {
|
|
// --- STATE 1: WAITING FOR WAVE ---
|
|
document.getElementById("intel-wave-label").innerText = "WAVE " + nextWaveIdx + ":";
|
|
|
|
// --- NEW: Wave End Hook (Story Popup & Tower Limits) ---
|
|
if (stg.cfg && stg.cfg.custom_waves) {
|
|
var customData = stg.cfg.custom_waves[nextWaveIdx];
|
|
if (customData && (!this.scene._prep_done_for_wave || this.scene._prep_done_for_wave !== nextWaveIdx)) {
|
|
this.scene._prep_done_for_wave = nextWaveIdx;
|
|
|
|
// 1. Tower Availability Override
|
|
if (TD.factionData && TD.current_faction) {
|
|
var towers = TD.factionData[TD.current_faction];
|
|
for (var t = 1; t <= 11; t++) {
|
|
var tStatus = customData.towers["tower_" + t] || "unlocked";
|
|
var btn = document.getElementById("btn-buy-tower-" + t);
|
|
if (tStatus === "hidden") {
|
|
if (btn) btn.style.setProperty("display", "none", "important");
|
|
} else {
|
|
if (btn) btn.style.removeProperty("display");
|
|
if (towers[t-1]) {
|
|
// Only lock it if the player hasn't already paid to unlock it!
|
|
if (tStatus === "unlocked") {
|
|
towers[t-1].is_unlocked = true;
|
|
} else if (tStatus === "locked" && towers[t-1].is_unlocked !== true) {
|
|
towers[t-1].is_unlocked = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
TD.loadFaction(TD.current_faction); // Refresh UI costs and locks
|
|
}
|
|
|
|
|
|
|
|
// 2. Story Popup Override
|
|
if (customData.story_enable && customData.story_data) {
|
|
var sData = customData.story_data[TD.current_faction];
|
|
// Fallback to "ALL" if faction-specific text is blank
|
|
if (!sData || (!sData.title && !sData.desc)) {
|
|
sData = customData.story_data["ALL"];
|
|
}
|
|
|
|
if (sData && (sData.title || sData.desc)) {
|
|
TD.is_paused = true; // Hard freeze engine
|
|
document.getElementById("campaign-intro-title").innerText = sData.title || "INCOMING TRANSMISSION";
|
|
|
|
// Convert both Enter keys and manually typed \n into proper HTML line breaks
|
|
document.getElementById("campaign-intro-desc").innerHTML = (sData.desc || "").replace(/\\n/g, "<br>").replace(/\n/g, "<br>");
|
|
|
|
document.getElementById("campaign-intro-modal-overlay").style.display = "flex";
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
// ---------------------------------------------------------
|
|
|
|
var wavesCfg = stg.config ? stg.config.waves : [];
|
|
|
|
// Dynamically update the configuration array in real-time before the wave starts
|
|
if (wavesCfg && (!this.scene._wave_generated_for || this.scene._wave_generated_for !== nextWaveIdx)) {
|
|
wavesCfg[nextWaveIdx] = TD.makeMonsters();
|
|
this.scene._wave_generated_for = nextWaveIdx;
|
|
this.scene._intel_chatter_generated = false; // Reset chatter flag for the next wave
|
|
}
|
|
|
|
var nextWaveData = wavesCfg[nextWaveIdx];
|
|
var monsterNames = [];
|
|
|
|
if (nextWaveData && nextWaveData.length > 0) {
|
|
for (var i = 0; i < nextWaveData.length; i++) {
|
|
var mQty = nextWaveData[i][0];
|
|
var mIdx = nextWaveData[i][1];
|
|
var mName = TD.getDefaultMonsterAttributes(mIdx).name;
|
|
monsterNames.push(mQty + "X " + mName.toUpperCase());
|
|
}
|
|
}
|
|
var monsterString = monsterNames.length > 0 ? monsterNames.join(", ") : "UNKNOWN THREATS";
|
|
|
|
// Bypass the queue and force the countdown directly into the ticker text instantly every frame
|
|
var tickerEl = document.getElementById("intel-ticker-text");
|
|
if (tickerEl) {
|
|
TD.typewriter_target = "<span style='color:#fff;'>T-MINUS:</span> " + sec + "s <span style='color:#6a7354;'>|</span> <span style='color:#fff;'>THREAT:</span> " + monsterString;
|
|
TD.typewriter_current = TD.typewriter_target; // Bypass typing animation for live timer
|
|
tickerEl.innerHTML = TD.typewriter_target;
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
// --- STATE 2: WAVE IN MOTION ---
|
|
document.getElementById("intel-wave-label").innerText = "WAVE " + currentWaveIdx + ":";
|
|
|
|
// Generate initial chatter when the wave starts
|
|
if (!this.scene._intel_chatter_generated && window.IntelData) {
|
|
this.scene._intel_chatter_generated = true;
|
|
|
|
if (typeof TD !== 'undefined' && TD.logEvent) {
|
|
// --- EARLY WARNING & COUNTER-MEASURE CHECK ---
|
|
var has_aa = false;
|
|
var has_detect = false;
|
|
var incoming_stealth = false;
|
|
var incoming_flyer = false;
|
|
|
|
// 1. Check what the player has built
|
|
if (this.map && this.map.buildings) {
|
|
TD.lang.each(this.map.buildings, function(b) {
|
|
if (b.is_valid) {
|
|
var vType = b.visual_type || b.type;
|
|
var isEarly = (b.type === "tower_1" || b.type === "tower_2" || b.type === "tower_3" || (b.id && (b.id.endsWith("_1") || b.id.endsWith("_2") || b.id.endsWith("_3"))));
|
|
if (!isEarly && (vType === "laser_gun" || vType === "LMG" || vType === "HMG" || vType === "rifle")) has_aa = true;
|
|
if (b.is_detector) has_detect = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 2. Generate SMART Organic Wave Report
|
|
var currentWaveData = (stg.config && stg.config.waves) ? stg.config.waves[currentWaveIdx] : null;
|
|
var threatPhrases = [];
|
|
|
|
// A. Tally and Consolidate identical enemies
|
|
var consolidatedWave = {};
|
|
if (currentWaveData) {
|
|
for (var k = 0; k < currentWaveData.length; k++) {
|
|
var mIdx = currentWaveData[k][1];
|
|
var mQty = currentWaveData[k][0];
|
|
if (!consolidatedWave[mIdx]) consolidatedWave[mIdx] = { qty: 0, attr: TD.getDefaultMonsterAttributes(mIdx) };
|
|
consolidatedWave[mIdx].qty += mQty;
|
|
|
|
// Track critical threats
|
|
if (consolidatedWave[mIdx].attr.is_flying || mIdx === 8 || mIdx === 9 || mIdx === 10) incoming_flyer = true;
|
|
if (consolidatedWave[mIdx].attr.is_stealthed || mIdx === 11) incoming_stealth = true;
|
|
}
|
|
}
|
|
|
|
// B. Convert to text based on the number of unique types
|
|
var uniqueTypes = Object.keys(consolidatedWave).length;
|
|
var monsterStr = "UNKNOWN HOSTILES";
|
|
|
|
if (uniqueTypes >= 3) {
|
|
// Too many types = Truncate to a Horde description
|
|
monsterStr = "A MASSIVE MIXED HORDE";
|
|
if (incoming_flyer && incoming_stealth) monsterStr += " (CLOAKED AND AERIAL THREATS EMBEDDED)";
|
|
else if (incoming_flyer) monsterStr += " (AERIAL THREATS EMBEDDED)";
|
|
else if (incoming_stealth) monsterStr += " (CLOAKED THREATS EMBEDDED)";
|
|
} else if (uniqueTypes > 0) {
|
|
// 1 or 2 types = List them specifically with human quantities
|
|
for (var key in consolidatedWave) {
|
|
var group = consolidatedWave[key];
|
|
var qtyStr = "A FEW";
|
|
if (group.qty === 1) qtyStr = "A LONE";
|
|
else if (group.qty >= 2 && group.qty <= 4) qtyStr = "A SMALL GROUP OF";
|
|
else if (group.qty >= 5 && group.qty <= 9) qtyStr = "A HEAVY PRESENCE OF";
|
|
else if (group.qty >= 10) qtyStr = "A MASSIVE SWARM OF";
|
|
|
|
threatPhrases.push(qtyStr + " " + group.attr.name.toUpperCase() + (group.qty > 1 && !group.attr.name.endsWith("S") ? "S" : ""));
|
|
}
|
|
monsterStr = threatPhrases.join(" AND ");
|
|
}
|
|
|
|
// C. Pull the faction-specific Mad-Libs sentence and inject the Smart string!
|
|
var baseSentence = window.IntelData.getEvent("wave_start", TD.current_faction);
|
|
var finalReport = baseSentence.replace("{M}", monsterStr);
|
|
TD.logEvent(finalReport);
|
|
|
|
// 3. Log Missing Counter-Measures (We skipped the "Now" alerts because the new summary includes them naturally)
|
|
|
|
if (incoming_flyer && !has_aa) TD.logEvent(window.IntelData.getEvent("tactical_no_antiair", "GENERAL"));
|
|
else if (incoming_flyer) TD.logEvent(window.IntelData.getEvent("threat_flying_now", "GENERAL"));
|
|
|
|
if (incoming_stealth && !has_detect) TD.logEvent(window.IntelData.getEvent("tactical_no_detector", "GENERAL"));
|
|
else if (incoming_stealth) TD.logEvent(window.IntelData.getEvent("threat_stealth_now", "GENERAL"));
|
|
|
|
// 4. Look Ahead to the NEXT wave for early prep warnings
|
|
var nextWaveData = (stg.config && stg.config.waves) ? stg.config.waves[currentWaveIdx + 1] : null;
|
|
if (nextWaveData) {
|
|
var next_stealth = false;
|
|
var next_flyer = false;
|
|
for (var n = 0; n < nextWaveData.length; n++) {
|
|
var nAttr = TD.getDefaultMonsterAttributes(nextWaveData[n][1]);
|
|
if (nAttr.is_flying || nextWaveData[n][1] === 8 || nextWaveData[n][1] === 9 || nextWaveData[n][1] === 10) next_flyer = true;
|
|
if (nAttr.is_stealthed || nextWaveData[n][1] === 11) next_stealth = true;
|
|
}
|
|
if (next_flyer && !incoming_flyer) TD.logEvent(window.IntelData.getEvent("threat_flying_prep", "GENERAL"));
|
|
if (next_stealth && !incoming_stealth) TD.logEvent(window.IntelData.getEvent("threat_stealth_prep", "GENERAL"));
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// 3. UI BUTTON STATES (Logic Locks)
|
|
var btnStartWave = document.getElementById("ui-btn-start-wave");
|
|
if (btnStartWave) {
|
|
// Disabled during combat
|
|
btnStartWave.disabled = waveInMotion;
|
|
btnStartWave.style.opacity = waveInMotion ? "0.3" : "1";
|
|
btnStartWave.style.cursor = waveInMotion ? "not-allowed" : "pointer";
|
|
}
|
|
|
|
var btnSlow = document.getElementById("ui-btn-slow");
|
|
var btnSpeed = document.getElementById("ui-btn-speed");
|
|
|
|
// Speed controls are DISABLED during "The Calm" (waiting phase)
|
|
var timeControlsActive = waveInMotion;
|
|
|
|
if (btnSlow) {
|
|
var canSlow = timeControlsActive && (Math.round(TD.global_speed * 10) > 2);
|
|
btnSlow.disabled = !canSlow;
|
|
btnSlow.style.opacity = !canSlow ? "0.3" : "1";
|
|
btnSlow.style.cursor = !canSlow ? "not-allowed" : "pointer";
|
|
}
|
|
|
|
if (btnSpeed) {
|
|
var canSpeed = timeControlsActive && (Math.round(TD.global_speed * 10) < 10);
|
|
btnSpeed.disabled = !canSpeed;
|
|
btnSpeed.style.opacity = !canSpeed ? "0.3" : "1";
|
|
btnSpeed.style.cursor = !canSpeed ? "not-allowed" : "pointer";
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
TD.Panel = function (id, cfg) { var panel = new TD.Element(id, cfg); TD.lang.mix(panel, panel_obj); panel._init(cfg); return panel; };
|
|
|
|
var balloontip_obj = {
|
|
_init: function (cfg) { cfg = cfg || {}; this.scene = cfg.scene; },
|
|
caculatePos: function () {
|
|
var el = this.el; this.x = el.cx + 0.5; this.y = el.cy + 0.5;
|
|
if (this.x + this.width > this.scene.stage.width - TD.padding) { this.x = this.x - this.width; }
|
|
this.px = this.x + 5 * _TD.retina; this.py = this.y + 4 * _TD.retina;
|
|
},
|
|
msg: function (txt, el) {
|
|
this.text = txt; var ctx = TD.ctx; ctx.font = "bold " + (12 * _TD.retina) + "px 'Courier New'";
|
|
|
|
// Support multi-line width calculation properly
|
|
var lines = txt.split('\n');
|
|
var maxW = 0;
|
|
for (var i=0; i<lines.length; i++) {
|
|
var w = ctx.measureText(lines[i]).width;
|
|
if (w > maxW) maxW = w;
|
|
}
|
|
this.width = maxW + 20 * _TD.retina;
|
|
this.height = (lines.length * 15 + 8) * _TD.retina;
|
|
|
|
if (el && el.cx && el.cy) { this.el = el; this.caculatePos(); this.show(); }
|
|
},
|
|
step: function () { if (!this.el || !this.el.is_valid) { this.hide(); return; } if (this.el.is_monster) { this.caculatePos(); } },
|
|
render: function () {
|
|
if (!this.el) return; var ctx = TD.ctx;
|
|
|
|
if (this.el.is_monster) {
|
|
// --- V.A.T.S. STYLE RENDER FOR MONSTERS ---
|
|
var w = this.width;
|
|
var h = this.height + 15 * _TD.retina; // extra space for HP bar
|
|
|
|
// Main Box
|
|
ctx.fillStyle = "rgba(0, 40, 0, 0.7)";
|
|
ctx.fillRect(this.x, this.y, w, h);
|
|
ctx.strokeStyle = "rgba(0, 255, 0, 0.2)";
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeRect(this.x, this.y, w, h);
|
|
|
|
// Corner Brackets
|
|
ctx.strokeStyle = "#0f0";
|
|
ctx.lineWidth = 2 * _TD.retina;
|
|
var bS = 8 * _TD.retina;
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.x, this.y + bS); ctx.lineTo(this.x, this.y); ctx.lineTo(this.x + bS, this.y); // Top Left
|
|
ctx.moveTo(this.x + w - bS, this.y); ctx.lineTo(this.x + w, this.y); ctx.lineTo(this.x + w, this.y + bS); // Top Right
|
|
ctx.moveTo(this.x, this.y + h - bS); ctx.lineTo(this.x, this.y + h); ctx.lineTo(this.x + bS, this.y + h); // Bottom Left
|
|
ctx.moveTo(this.x + w - bS, this.y + h); ctx.lineTo(this.x + w, this.y + h); ctx.lineTo(this.x + w, this.y + h - bS); // Bottom Right
|
|
ctx.stroke();
|
|
|
|
// Health Bar (Top)
|
|
var barW = w - 16 * _TD.retina;
|
|
var barH = 5 * _TD.retina;
|
|
var barX = this.x + 8 * _TD.retina;
|
|
var barY = this.y + 6 * _TD.retina;
|
|
var hpPercent = Math.max(0, this.el.life / this.el.life0);
|
|
|
|
ctx.fillStyle = "rgba(0,0,0,0.8)"; ctx.fillRect(barX, barY, barW, barH);
|
|
ctx.fillStyle = "#0f0"; ctx.fillRect(barX, barY, barW * hpPercent, barH);
|
|
ctx.lineWidth = 1; ctx.strokeStyle = "#0f0"; ctx.strokeRect(barX, barY, barW, barH);
|
|
|
|
// Text Lines
|
|
ctx.textAlign = "center"; ctx.textBaseline = "top"; ctx.fillStyle = "#0f0";
|
|
ctx.font = "bold " + (12 * _TD.retina) + "px 'Courier New'";
|
|
var lines = this.text.split('\n');
|
|
for (var i = 0; i < lines.length; i++) {
|
|
ctx.fillText(lines[i], this.x + w/2, this.y + 16 * _TD.retina + (i * 15 * _TD.retina));
|
|
}
|
|
} else {
|
|
// --- STANDARD YELLOW TOOLTIP FOR EVERYTHING ELSE ---
|
|
ctx.lineWidth = _TD.retina; ctx.fillStyle = "rgba(255, 255, 0, 0.5)"; ctx.strokeStyle = "rgba(222, 222, 0, 0.9)";
|
|
ctx.beginPath(); ctx.rect(this.x, this.y, this.width, this.height); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.textAlign = "left"; ctx.textBaseline = "top"; ctx.fillStyle = "#000"; ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'";
|
|
|
|
var lines = this.text.split('\n');
|
|
for (var i = 0; i < lines.length; i++) {
|
|
ctx.fillText(lines[i], this.px, this.py + (i * 15 * _TD.retina));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
TD.BalloonTip = function (id, cfg) { var balloontip = new TD.Element(id, cfg); TD.lang.mix(balloontip, balloontip_obj); balloontip._init(cfg); return balloontip; };
|
|
|
|
var button_obj = {
|
|
_init: function (cfg) {
|
|
cfg = cfg || {}; this.text = cfg.text; this.onClick = cfg.onClick || TD.lang.nullFunc; this.x = cfg.x; this.y = cfg.y; this.width = cfg.width || 80 * _TD.retina; this.height = cfg.height || 30 * _TD.retina; this.font_x = this.x + 8 * _TD.retina; this.font_y = this.y + 9 * _TD.retina; this.scene = cfg.scene; this.desc = cfg.desc || "";
|
|
this.addToScene(this.scene, this.step_level, this.render_level); this.caculatePos();
|
|
},
|
|
onEnter: function () { TD.mouseHand(true); if (this.desc) { this.scene.panel.balloontip.msg(this.desc, this); } },
|
|
onOut: function () { TD.mouseHand(false); if (this.scene.panel.balloontip.el == this) { this.scene.panel.balloontip.hide(); } },
|
|
render: function () {
|
|
var ctx = TD.ctx; ctx.lineWidth = 2 * _TD.retina; ctx.fillStyle = this.is_hover ? "#eee" : "#ccc"; ctx.strokeStyle = "#999"; ctx.beginPath(); ctx.rect(this.x, this.y, this.width, this.height); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.textAlign = "left"; ctx.textBaseline = "top"; ctx.fillStyle = "#000"; ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; ctx.beginPath(); ctx.fillText(this.text, this.font_x, this.font_y); ctx.closePath(); ctx.fill();
|
|
}
|
|
};
|
|
TD.Button = function (id, cfg) { cfg.on_events = ["enter", "out", "click"]; var button = new TD.Element(id, cfg); TD.lang.mix(button, button_obj); button._init(cfg); return button; };
|
|
|
|
var gameover_obj = {
|
|
_init: function (cfg) { this.panel = cfg.panel; this.scene = cfg.scene; this.addToScene(this.scene, 1, 9); },
|
|
render: function () {
|
|
this.panel.btn_pause.hide(); this.panel.btn_upgrade.hide(); this.panel.btn_sell.hide(); this.panel.btn_build.hide(); this.panel.btn_restart.show();
|
|
|
|
// Inject current stats into the Game Over overlay
|
|
var elWaves = document.getElementById("go-stat-waves");
|
|
if (elWaves && this.scene) elWaves.innerText = this.scene.wave || 0;
|
|
|
|
var elScore = document.getElementById("go-stat-score");
|
|
if (elScore && typeof TD !== 'undefined') elScore.innerText = TD.score || 0;
|
|
|
|
var elCaps = document.getElementById("go-stat-caps");
|
|
if (elCaps && typeof TD !== 'undefined') elCaps.innerText = TD.money || 0;
|
|
|
|
var elShots = document.getElementById("go-stat-shots");
|
|
if (elShots && typeof TD !== 'undefined') elShots.innerText = TD.total_shots || 0;
|
|
|
|
var elMvp = document.getElementById("go-stat-mvp");
|
|
if (elMvp && this.scene && this.scene.map) {
|
|
var mvp = null;
|
|
var maxKills = -1;
|
|
TD.lang.each(this.scene.map.buildings, function(b) {
|
|
if (b.is_weapon && b.killed > maxKills) {
|
|
maxKills = b.killed;
|
|
mvp = b;
|
|
}
|
|
});
|
|
if (mvp && maxKills > 0) {
|
|
elMvp.innerText = mvp.name.toUpperCase() + " [" + maxKills + " KILLS]";
|
|
} else {
|
|
elMvp.innerText = "NONE";
|
|
}
|
|
}
|
|
|
|
// Replaced Canvas drawing with the HTML overlay trigger
|
|
var goText = document.getElementById("game-over-text");
|
|
|
|
if (goText) goText.style.display = "block";
|
|
}
|
|
};
|
|
|
|
TD.GameOver = function (id, cfg) { var obj = new TD.Element(id, cfg); TD.lang.mix(obj, gameover_obj); obj._init(cfg); return obj; };
|
|
TD.recover = function (n) { TD.life_recover = n; TD.log("life recover: " + n); };
|
|
});
|
|
|
|
_TD.a.push(function (TD) {
|
|
function lineTo2(ctx, x0, y0, x1, y1, len) {
|
|
var x2, y2, a, b, p, xt, a2, b2, c2;
|
|
if (x0 == x1) { x2 = x0; y2 = y1 > y0 ? y0 + len : y0 - len; }
|
|
else if (y0 == y1) { y2 = y0; x2 = x1 > x0 ? x0 + len : x0 - len; }
|
|
else {
|
|
a = (y0 - y1) / (x0 - x1); b = y0 - x0 * a; a2 = a * a + 1; b2 = 2 * (a * (b - y0) - x0); c2 = Math.pow(b - y0, 2) + x0 * x0 - Math.pow(len, 2); p = Math.pow(b2, 2) - 4 * a2 * c2;
|
|
if (p < 0) { return [0, 0]; } p = Math.sqrt(p); xt = (-b2 + p) / (2 * a2);
|
|
if ((x1 - x0 > 0 && xt - x0 > 0) || (x1 - x0 < 0 && xt - x0 < 0)) { x2 = xt; y2 = a * x2 + b; }
|
|
else { x2 = (-b2 - p) / (2 * a2); y2 = a * x2 + b; }
|
|
}
|
|
ctx.lineCap = "round"; ctx.moveTo(x0, y0); ctx.lineTo(x2, y2); return [x2, y2];
|
|
}
|
|
var renderFunctions = {
|
|
"rifle": function (b, ctx, map, gs, gs2) {
|
|
var target_position = b.getTargetPosition();
|
|
ctx.fillStyle = "#886644"; ctx.strokeStyle = "#000"; ctx.beginPath(); ctx.lineWidth = _TD.retina; ctx.arc(b.cx, b.cy, 6 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.lineWidth = 2 * _TD.retina; ctx.beginPath(); ctx.moveTo(b.cx, b.cy); b.muzzle = lineTo2(ctx, b.cx, b.cy, target_position[0], target_position[1], gs2); ctx.closePath(); ctx.stroke();
|
|
ctx.lineWidth = _TD.retina; ctx.fillStyle = "#333"; ctx.beginPath(); ctx.arc(b.cx, b.cy, 4 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
},
|
|
"cannon": function (b, ctx, map, gs, gs2) {
|
|
|
|
var target_position = b.getTargetPosition();
|
|
ctx.fillStyle = "#393"; ctx.strokeStyle = "#000"; ctx.beginPath(); ctx.lineWidth = _TD.retina; ctx.arc(b.cx, b.cy, gs2 - 5, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.lineWidth = 3 * _TD.retina; ctx.beginPath(); ctx.moveTo(b.cx, b.cy); b.muzzle = lineTo2(ctx, b.cx, b.cy, target_position[0], target_position[1], gs2); ctx.closePath(); ctx.stroke();
|
|
ctx.lineWidth = _TD.retina; ctx.fillStyle = "#060"; ctx.beginPath(); ctx.arc(b.cx, b.cy, 7 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = "#cec"; ctx.beginPath(); ctx.arc(b.cx + 2, b.cy - 2, 3 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill();
|
|
},
|
|
"LMG": function (b, ctx, map, gs, gs2) {
|
|
var target_position = b.getTargetPosition();
|
|
ctx.fillStyle = "#36f"; ctx.strokeStyle = "#000"; ctx.beginPath(); ctx.lineWidth = _TD.retina; ctx.arc(b.cx, b.cy, 7 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.lineWidth = 2 * _TD.retina; ctx.beginPath(); ctx.moveTo(b.cx, b.cy); b.muzzle = lineTo2(ctx, b.cx, b.cy, target_position[0], target_position[1], gs2); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.lineWidth = _TD.retina; ctx.fillStyle = "#66c"; ctx.beginPath(); ctx.arc(b.cx, b.cy, 5 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = "#ccf"; ctx.beginPath(); ctx.arc(b.cx + 1, b.cy - 1, 2 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill();
|
|
},
|
|
"HMG": function (b, ctx, map, gs, gs2) {
|
|
var target_position = b.getTargetPosition();
|
|
ctx.fillStyle = "#933"; ctx.strokeStyle = "#000"; ctx.beginPath(); ctx.lineWidth = _TD.retina; ctx.arc(b.cx, b.cy, gs2 - 2, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.lineWidth = 5 * _TD.retina; ctx.beginPath(); ctx.moveTo(b.cx, b.cy); b.muzzle = lineTo2(ctx, b.cx, b.cy, target_position[0], target_position[1], gs2); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.lineWidth = _TD.retina; ctx.fillStyle = "#630"; ctx.beginPath(); ctx.arc(b.cx, b.cy, gs2 - 5 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = "#960"; ctx.beginPath(); ctx.arc(b.cx + 1, b.cy - 1, 8 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill();
|
|
ctx.fillStyle = "#fcc"; ctx.beginPath(); ctx.arc(b.cx + 3, b.cy - 3, 4 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill();
|
|
},
|
|
"wall": function (b, ctx, map, gs, gs2) {
|
|
ctx.lineWidth = _TD.retina; ctx.fillStyle = "#666"; ctx.strokeStyle = "#000"; ctx.fillRect(b.cx - gs2 + 1, b.cy - gs2 + 1, gs - 1, gs - 1);
|
|
ctx.beginPath(); ctx.moveTo(b.cx - gs2 + 0.5, b.cy - gs2 + 0.5); ctx.lineTo(b.cx - gs2 + 0.5, b.cy + gs2 + 0.5); ctx.lineTo(b.cx + gs2 + 0.5, b.cy + gs2 + 0.5); ctx.lineTo(b.cx + gs2 + 0.5, b.cy - gs2 + 0.5); ctx.lineTo(b.cx - gs2 + 0.5, b.cy - gs2 + 0.5); ctx.moveTo(b.cx - gs2 + 0.5, b.cy + gs2 + 0.5); ctx.lineTo(b.cx + gs2 + 0.5, b.cy - gs2 + 0.5); ctx.moveTo(b.cx - gs2 + 0.5, b.cy - gs2 + 0.5); ctx.lineTo(b.cx + gs2 + 0.5, b.cy + gs2 + 0.5); ctx.closePath(); ctx.stroke();
|
|
},
|
|
"laser_gun": function (b, ctx) {
|
|
ctx.fillStyle = "#f00"; ctx.strokeStyle = "#000"; ctx.beginPath(); ctx.lineWidth = _TD.retina;
|
|
ctx.moveTo(b.cx, b.cy - 10 * _TD.retina); ctx.lineTo(b.cx - 8.66 * _TD.retina, b.cy + 5 * _TD.retina); ctx.lineTo(b.cx + 8.66 * _TD.retina, b.cy + 5 * _TD.retina); ctx.lineTo(b.cx, b.cy - 10 * _TD.retina); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = "#60f"; ctx.beginPath(); ctx.arc(b.cx, b.cy, 7 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = "#000"; ctx.beginPath(); ctx.arc(b.cx, b.cy, 3 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill();
|
|
ctx.fillStyle = "#666"; ctx.beginPath(); ctx.arc(b.cx + 1, b.cy - 1, _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill();
|
|
ctx.lineWidth = 3 * _TD.retina; ctx.beginPath(); ctx.moveTo(b.cx, b.cy); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
}
|
|
};
|
|
TD.renderBuilding = function (building) {
|
|
var ctx = TD.ctx, map = building.map, gs = TD.grid_size, gs2 = TD.grid_size / 2;
|
|
|
|
// --- NEW: PSYCHO RAGE GLOW (Smooth Breathing) ---
|
|
if (TD.psycho_active > 0 && building.is_weapon && !building.is_pre_building) {
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
// Increased radius (gs2 * 1.1) so it spills out past the largest towers
|
|
ctx.arc(building.cx, building.cy, gs2 * 1.1, 0, Math.PI * 2);
|
|
|
|
// Smooth sine wave: (sin(x) + 1) / 2 creates a perfect 0.0 to 1.0 oscillation
|
|
var pulse = (Math.sin(TD.iframe / 10) + 1) / 2;
|
|
|
|
ctx.shadowBlur = 15 + (pulse * 15); // Blurs smoothly between 15px and 30px
|
|
ctx.shadowColor = "#ff0000";
|
|
// Opacity pulses smoothly between 0.15 (dim) and 0.45 (bright)
|
|
ctx.fillStyle = "rgba(255, 0, 0, " + (0.15 + (pulse * 0.3)) + ")";
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
|
|
|
|
// NEW: Draw aerial drop shadow AND ground anchor pad
|
|
if (building.is_flying && !building.is_pre_building) {
|
|
// 1. Draw the stationary physical anchor pad on the ground
|
|
ctx.fillStyle = "#222222";
|
|
ctx.strokeStyle = "#111111";
|
|
ctx.lineWidth = 2 * _TD.retina;
|
|
ctx.fillRect(building.x + 2, building.y + 2, gs - 4, gs - 4);
|
|
ctx.strokeRect(building.x + 2, building.y + 2, gs - 4, gs - 4);
|
|
|
|
// 2. Draw the floating drop shadow
|
|
ctx.save();
|
|
ctx.translate(building.cx, building.cy - building.hover_y);
|
|
ctx.scale(1, 0.5); // Flatten the circle into a perspective ellipse
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, 15 * _TD.retina, 0, Math.PI * 2, true);
|
|
ctx.fillStyle = "rgba(0,0,0,0.6)";
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
|
|
|
|
// Use visual_type if defined, otherwise fallback to the type, otherwise wall
|
|
var renderType = building.visual_type || building.type;
|
|
(renderFunctions[renderType] || renderFunctions["wall"])(building, ctx, map, gs, gs2);
|
|
}
|
|
|
|
});
|
|
|
|
|
|
_TD.a.push(function (TD) {
|
|
var building_obj = {
|
|
_init: function (cfg) {
|
|
this.is_selected = false; this.level = 1; this.xp = 0;
|
|
this.xp_needed = 50; // Starts strictly at 50 XP
|
|
this.killed = 0; this.target = null;
|
|
|
|
cfg = cfg || {}; this.map = cfg.map || null; this.grid = cfg.grid || null; this.bullet_type = cfg.bullet_type || 1;
|
|
this.type = cfg.type; this.speed = cfg.speed; this.bullet_speed = cfg.bullet_speed; this.is_pre_building = !!cfg.is_pre_building; this.blink = this.is_pre_building; this.wait_blink = this._default_wait_blink = 20; this.is_weapon = (this.type != "wall");
|
|
|
|
// 1. Fetch the exact stats from the Faction Data
|
|
var o = TD.getDefaultBuildingAttributes(this.type);
|
|
|
|
// 2. Overwrite the default config with the faction specific stats
|
|
TD.lang.mix(this, o, true); // The 'true' forces the mix function to overwrite properties!
|
|
|
|
// --- NEW: APPLY S.P.E.C.I.A.L. PASSIVES ---
|
|
if (TD.perks) {
|
|
if (TD.perks.C) this.cost = Math.floor(this.cost * 0.85); // Charisma: 15% cheaper
|
|
if (TD.perks.S) this.damage = Math.floor(this.damage * 1.15); // Strength: 15% more damage
|
|
if (TD.perks.P) this.range += 2; // Perception: +2 Range
|
|
if (TD.perks.I) this.xp_needed = Math.floor(this.xp_needed * 0.85); // Intelligence: 15% less XP to level
|
|
if (TD.perks.A) this.speed *= 1.15; // Agility: 15% faster fire rate
|
|
}
|
|
|
|
this.range_px = this.range * TD.grid_size; this.money = this.cost;
|
|
this.caculatePos();
|
|
},
|
|
|
|
|
|
getUpgradeCost: function () { return Math.floor(this.money * 0.75); },
|
|
getSellMoney: function () { return Math.floor(this.money * 0.5) || 1; },
|
|
getBuyCost: function () { return Math.floor(this.money * 0.5) || 1; },
|
|
toggleSelected: function () {
|
|
this.is_selected = !this.is_selected; this.grid.hightLight(this.is_selected); var _this = this;
|
|
if (this.is_selected) {
|
|
this.map.eachBuilding(function (obj) { obj.is_selected = obj == _this; });
|
|
(this.map.is_main_map ? this.scene.panel_map : this.scene.map).eachBuilding(function (obj) { obj.is_selected = false; obj.grid.hightLight(false); });
|
|
this.map.selected_building = this;
|
|
if (!this.map.is_main_map) { this.scene.map.preBuild(this.type); } else { this.scene.map.cancelPreBuild(); }
|
|
} else {
|
|
if (this.map.selected_building == this) this.map.selected_building = null;
|
|
if (!this.map.is_main_map) { this.scene.map.cancelPreBuild(); }
|
|
}
|
|
if (this.map.is_main_map) {
|
|
if (this.map.selected_building) { this.scene.panel.btn_upgrade.show(); this.scene.panel.btn_sell.show(); this.scene.panel.btn_build.show(); this.updateBtnDesc(); }
|
|
else { this.scene.panel.btn_upgrade.hide(); this.scene.panel.btn_sell.hide(); this.scene.panel.btn_build.hide(); }
|
|
}
|
|
},
|
|
updateBtnDesc: function () {
|
|
this.scene.panel.btn_upgrade.desc = "Upgrade " + this.name + " to Level " + (this.level + 1) + " (Cost: " + this.getUpgradeCost() + "c)";
|
|
this.scene.panel.btn_sell.desc = "Sell " + this.name + " for " + this.getSellMoney() + "c";
|
|
this.scene.panel.btn_build.desc = "Build " + this.name + " for " + this.getBuyCost() + "c";
|
|
},
|
|
|
|
locate: function (grid) {
|
|
this.grid = grid; this.map = grid.map; this.cx = this.grid.cx; this.cy = this.grid.cy; this.x = this.grid.x; this.y = this.grid.y; this.x2 = this.grid.x2; this.y2 = this.grid.y2; this.width = this.grid.width; this.height = this.grid.height; this.px = this.x + 0.5; this.py = this.y + 0.5;
|
|
this.wait_blink = this._default_wait_blink; this._fire_wait = Math.floor(Math.max(TD.exp_fps / (this.speed * (TD.global_speed / 0.2)), 1)); this._fire_wait2 = this._fire_wait;
|
|
|
|
// NEW: Flight Path Initialization
|
|
if (this.is_flying) {
|
|
|
|
this.flight_angle = Math.random() * Math.PI * 2; // Random start point on path
|
|
this.anchor_cx = this.cx;
|
|
this.anchor_cy = this.cy;
|
|
this.flight_radius = TD.grid_size * 1.2; // roughly 3x3 area
|
|
this.flight_speed = 0.03 + Math.random() * 0.02; // Orbit speed
|
|
this.hover_y = -40 * _TD.retina; // Altitude offset
|
|
}
|
|
},
|
|
|
|
remove: function () { if (this.grid && this.grid.building && this.grid.building == this) this.grid.building = null; this.hide(); this.del(); },
|
|
findTarget: function () {
|
|
if (!this.is_weapon || this.is_pre_building || !this.grid) return;
|
|
var current_range = this.eff_range || this.range;
|
|
var cx = this.cx, cy = this.cy, range2 = Math.pow(current_range * TD.grid_size, 2);
|
|
|
|
var vType = this.visual_type || this.type;
|
|
var canHitAir = (vType === "laser_gun" || vType === "LMG" || vType === "HMG" || vType === "rifle");
|
|
|
|
// --- NEW: TOWER ANTI-AIR LIMITS ---
|
|
// Prevent early game towers (1, 2, 3) from shooting flyers, regardless of weapon upgrades
|
|
if (this.type === "tower_1" || this.type === "tower_2" || this.type === "tower_3") {
|
|
canHitAir = false;
|
|
}
|
|
|
|
var valid_targets = [];
|
|
|
|
TD.lang.each(this.map.monsters, function (obj) {
|
|
if (obj.is_valid) {
|
|
var in_range = Math.pow(obj.cx - cx, 2) + Math.pow(obj.cy - cy, 2) <= range2;
|
|
var isFlying = !!obj.is_flying || obj.idx === 8 || obj.idx === 9 || obj.idx === 10;
|
|
var isStealthed = !!obj.is_stealthed || obj.idx === 11 || obj.is_burrowing; // Mole rats hide too!
|
|
|
|
if (in_range) {
|
|
if (isFlying && !canHitAir) return; // Anti-Air restriction
|
|
if (isStealthed && !obj.is_revealed) return; // Cannot shoot invisible/burrowed targets unless revealed
|
|
valid_targets.push(obj);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (valid_targets.length === 0) {
|
|
this.target = null;
|
|
return;
|
|
}
|
|
|
|
// 2. Sort by player-selected targeting mode (default: closest)
|
|
var mode = this.target_mode || "closest";
|
|
valid_targets.sort(function(a, b) {
|
|
if (mode === "strongest") return b.life - a.life;
|
|
if (mode === "weakest") return a.life - b.life;
|
|
if (mode === "leader") return a.way.length - b.way.length; // Fewer steps to exit = further ahead
|
|
|
|
// Fallback / "closest"
|
|
var distA = Math.pow(a.cx - cx, 2) + Math.pow(a.cy - cy, 2);
|
|
var distB = Math.pow(b.cx - cx, 2) + Math.pow(b.cy - cy, 2);
|
|
return distA - distB;
|
|
});
|
|
|
|
var new_target = valid_targets[0];
|
|
|
|
// --- NEW: ANNOUNCE TARGET ACQUISITION ---
|
|
if (this.target !== new_target && new_target && window.IntelData && TD.Announcer) {
|
|
// If it's a Boss (2000+ HP), it's a Critical "Hero Duel" announcement
|
|
if (new_target.life0 >= 2000) {
|
|
TD.Announcer.add(window.IntelData.getEvent("intercept", TD.current_faction, this.name, new_target.name), 5);
|
|
}
|
|
// Otherwise, normal priority, and we throttle it so cheap towers don't spam the log
|
|
else if (Math.random() < 0.1) {
|
|
TD.Announcer.add(window.IntelData.getEvent("intercept", TD.current_faction, this.name, new_target.name), 3);
|
|
}
|
|
}
|
|
|
|
this.target = new_target;
|
|
},
|
|
|
|
getTargetPosition: function () {
|
|
|
|
if (!this.target) { var grid = this.map.is_main_map ? this.map.entrance : this.grid; return [grid.cx, grid.cy]; }
|
|
return [this.target.cx, this.target.cy];
|
|
},
|
|
fire: function () {
|
|
var _this = this;
|
|
|
|
// --- DETECTOR PULSE ---
|
|
if (this.is_detector) {
|
|
new TD.RadarPulse("radar-" + TD.lang.rndStr(), { cx: this.cx, cy: this.cy, r: this.range_px / _TD.retina, step_level: 1, render_level: 9, color: "#00ffff", scene: this.map.scene, map: this.map, time: 1.0 });
|
|
return;
|
|
}
|
|
if (!this.target || !this.target.is_valid) return;
|
|
|
|
if (typeof TD !== 'undefined') {
|
|
TD.total_shots = (TD.total_shots || 0) + 1; // Tracks actual attacks
|
|
TD.last_fire_frame = TD.iframe; // NEW: Universal fire tracker for the dead-zone logic
|
|
}
|
|
|
|
// Use dynamic burst count if the upgrade provided one, otherwise fallback to name-based logic
|
|
var burst_count = this.burst_count || 1;
|
|
if (!this.burst_count && this.name && (this.name.indexOf("Trooper") > -1 || this.name.indexOf("Initiate") > -1 || this.name.indexOf("Settler") > -1 || this.name.indexOf("Scum") > -1)) burst_count = 3;
|
|
|
|
this._is_bursting = true;
|
|
for (var i = 0; i < burst_count; i++) {
|
|
setTimeout(function() {
|
|
if (!_this.is_valid || !_this.target || !_this.target.is_valid) return;
|
|
|
|
// --- NEW: SPECIAL EFFECTS FIRE HOOK ---
|
|
// Let the isolated handler take over if this tower has advanced firing logic
|
|
if (_this.has_special && typeof TD.SpecialEffects !== 'undefined' && TD.SpecialEffects.onFire(_this)) {
|
|
return; // If the handler returns true, it took care of shooting, so we skip standard logic
|
|
}
|
|
|
|
if (_this.visual_type == "laser_gun") { _this.target.beHit(_this, _this.damage); return; }
|
|
var muzzle = _this.muzzle || [_this.cx, _this.cy], cx = muzzle[0], cy = muzzle[1];
|
|
var current_damage = _this.eff_damage || _this.damage;
|
|
new TD.Bullet(null, { building: _this, damage: current_damage, target: _this.target, speed: _this.bullet_speed, x: cx, y: cy, pierce_count: _this.pierce_count });
|
|
|
|
// --- PLAY SOUND EFFECT ---
|
|
if (_this.sound_type) {
|
|
TD.playSound(_this.sound_type);
|
|
}
|
|
}, i * 150);
|
|
}
|
|
setTimeout(function() { _this._is_bursting = false; }, burst_count * 150);
|
|
},
|
|
|
|
tryToFire: function () {
|
|
// Detectors fire automatically without a target
|
|
if (this.is_detector) {
|
|
this._fire_wait--;
|
|
if (this._fire_wait <= 0) { this._fire_wait = this._fire_wait2; this.fire(); }
|
|
return;
|
|
}
|
|
|
|
if (!this.is_weapon || !this.target || this._is_bursting) return;
|
|
|
|
if (TD.psycho_active > 0) this._fire_wait -= 1.50;
|
|
else this._fire_wait--;
|
|
|
|
if (this._fire_wait <= 0) {
|
|
this._fire_wait = this._fire_wait2;
|
|
this.fire();
|
|
}
|
|
},
|
|
|
|
_upgrade2: function (k) {
|
|
if (!this._upgrade_records[k]) this._upgrade_records[k] = this[k];
|
|
var v = this._upgrade_records[k], mk = "max_" + k, uk = "_upgrade_rule_" + k, uf = this[uk] || TD.default_upgrade_rule;
|
|
if (!v || isNaN(v)) return;
|
|
v = uf(this.level, v); if (this[mk] && !isNaN(this[mk]) && this[mk] < v) v = this[mk];
|
|
this._upgrade_records[k] = v; this[k] = Math.floor(v);
|
|
},
|
|
upgrade: function () {
|
|
if (!this._upgrade_records) this._upgrade_records = {};
|
|
var attrs = ["damage", "range", "speed", "life", "shield"], i, l = attrs.length;
|
|
for (i = 0; i < l; i++) this._upgrade2(attrs[i]);
|
|
this.level++; this.range_px = this.range * TD.grid_size;
|
|
},
|
|
tryToUpgrade: function (btn) {
|
|
var cost = this.getUpgradeCost(), msg = "";
|
|
if (cost > TD.money) { msg = TD._t("not_enough_money", [cost]); }
|
|
else { TD.money -= cost; this.money += cost; this.upgrade(); msg = TD._t("upgrade_success", [TD._t("building_name_" + this.type), this.level, this.getUpgradeCost()]); }
|
|
this.updateBtnDesc(); this.scene.panel.balloontip.msg(msg, btn);
|
|
},
|
|
tryToSell: function () {
|
|
if (!this.is_valid) return;
|
|
TD.money += this.getSellMoney(); this.grid.removeBuilding(); this.is_valid = false; this.map.selected_building = null; this.map.select_hl.hide(); this.map.checkHasWeapon(); this.scene.panel.btn_upgrade.hide(); this.scene.panel.btn_sell.hide(); this.scene.panel.btn_build.hide(); this.scene.panel.balloontip.hide();
|
|
},
|
|
|
|
gainXp: function (amount) {
|
|
if (!this.is_weapon) return;
|
|
|
|
// --- NEW: Level 100 Cap ---
|
|
if (this.level >= 100) return;
|
|
|
|
this.xp += amount;
|
|
if (this.xp >= this.xp_needed) {
|
|
this.xp -= this.xp_needed;
|
|
this.level++;
|
|
// Scales by exactly +25 XP per level (Level 1->2 is 50, 2->3 is 75, etc.)
|
|
this.xp_needed = 50 + (this.level - 1) * 25;
|
|
|
|
// --- NEW: Level Up Floating Text ---
|
|
if (this.map && this.map.scene) {
|
|
new TD.FloatingText("lvlup-" + TD.lang.rndStr(), {
|
|
cx: this.cx,
|
|
cy: this.cy - (TD.grid_size / 2), // Start slightly above the tower
|
|
text: "LVL UP ⬆",
|
|
color: "#00ff00", // Bright Green
|
|
size: 14,
|
|
life: 48, // Lasts a full 2 seconds
|
|
map: this.map
|
|
});
|
|
|
|
if (typeof TD.logEvent === 'function') {
|
|
if (this.level === 2) TD.logEvent(this.name + " UNIT PROMOTED TO VETERAN STATUS");
|
|
else if (this.level === 4) TD.logEvent(this.name + " UNIT ACHIEVED ELITE COMBAT RATING");
|
|
}
|
|
}
|
|
|
|
// Auto-level base stats slightly
|
|
|
|
this.damage = Math.floor(this.damage * 1.1) + 1;
|
|
this.range = this.range + 0.1;
|
|
this.range_px = this.range * TD.grid_size;
|
|
this.speed = this.speed * 1.05;
|
|
this._fire_wait2 = Math.floor(Math.max(TD.exp_fps / (this.speed * (TD.global_speed / 0.2)), 1)); // Recalculate fire rate
|
|
}
|
|
|
|
|
|
},
|
|
|
|
step: function () {
|
|
if (this.blink) { this.wait_blink--; if (this.wait_blink < -this._default_wait_blink) this.wait_blink = this._default_wait_blink; }
|
|
|
|
// NEW: Flight Mechanics (Lissajous Figure-8 curve)
|
|
if (this.is_flying && !this.is_pre_building && this.anchor_cx) {
|
|
this.flight_angle += this.flight_speed * TD.global_speed;
|
|
this.cx = this.anchor_cx + Math.cos(this.flight_angle) * this.flight_radius;
|
|
// Double the sine frequency to make it a Figure-8 pattern, and add the hover altitude
|
|
this.cy = this.anchor_cy + Math.sin(this.flight_angle * 2) * (this.flight_radius * 0.5) + this.hover_y;
|
|
}
|
|
|
|
// --- DYNAMIC AURA CALCULATION ---
|
|
this.eff_range = this.range;
|
|
this.eff_damage = this.damage;
|
|
this.eff_speed = this.speed;
|
|
|
|
if (this.grid && !this.is_pre_building) {
|
|
var adj = [
|
|
this.map.getGrid(this.grid.mx + 1, this.grid.my),
|
|
this.map.getGrid(this.grid.mx - 1, this.grid.my),
|
|
this.map.getGrid(this.grid.mx, this.grid.my + 1),
|
|
this.map.getGrid(this.grid.mx, this.grid.my - 1)
|
|
];
|
|
|
|
for (var i = 0; i < adj.length; i++) {
|
|
if (adj[i] && adj[i].building && adj[i].building.aura_type) {
|
|
if (adj[i].building.aura_type === "range") this.eff_range += 1;
|
|
if (adj[i].building.aura_type === "speed") this.eff_speed *= 1.15;
|
|
if (adj[i].building.aura_type === "damage") this.eff_damage = Math.floor(this.eff_damage * 1.30);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sync visual range if hovered
|
|
this.range_px = this.eff_range * TD.grid_size;
|
|
// Update fire rate math dynamically
|
|
this._fire_wait2 = Math.floor(Math.max(TD.exp_fps / (this.eff_speed * (TD.global_speed / 0.2)), 1));
|
|
|
|
this.findTarget(); this.tryToFire();
|
|
|
|
// --- UPGRADE EFFECTS TICK ---
|
|
if (typeof TD.SpecialEffects !== 'undefined') {
|
|
TD.SpecialEffects.onStep(this);
|
|
}
|
|
},
|
|
|
|
|
|
|
|
render: function () {
|
|
if (!this.is_visiable || this.wait_blink < 0) return;
|
|
|
|
// NEW: If on the main map, building rendering is handled by the Y-Sort queue in Map.render
|
|
if (this.map.is_main_map && !this.is_pre_building) return;
|
|
|
|
var ctx = TD.ctx;
|
|
|
|
TD.renderBuilding(this);
|
|
// NEW: Added "this.is_hover" to the condition so it draws the range circle when moused over
|
|
|
|
var ctx = TD.ctx; TD.renderBuilding(this);
|
|
// NEW: Added "this.is_hover" to the condition so it draws the range circle when moused over
|
|
if (this.map.is_main_map && (this.is_selected || this.is_hover || (this.is_pre_building) || this.map.show_all_ranges) && this.is_weapon && this.range > 0 && this.grid) {
|
|
ctx.lineWidth = _TD.retina; ctx.fillStyle = "rgba(187, 141, 32, 0.15)"; ctx.strokeStyle = "#bb8d20"; ctx.beginPath(); ctx.arc(this.cx, this.cy, this.range_px, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
}
|
|
|
|
if (this.type == "laser_gun" && this.target && this.target.is_valid) {
|
|
ctx.lineWidth = 3 * _TD.retina; ctx.strokeStyle = "rgba(50, 50, 200, 0.5)"; ctx.beginPath(); ctx.moveTo(this.cx, this.cy); ctx.lineTo(this.target.cx, this.target.cy); ctx.closePath(); ctx.stroke();
|
|
ctx.lineWidth = _TD.retina; ctx.strokeStyle = "rgba(150, 150, 255, 0.5)"; ctx.beginPath(); ctx.lineTo(this.cx, this.cy); ctx.closePath(); ctx.stroke();
|
|
}
|
|
},
|
|
|
|
onEnter: function () {
|
|
if (this.is_pre_building || !this.map.is_main_map) return;
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (tooltip) {
|
|
// Determine Current Upgrades
|
|
var p1Name = "None"; var p2Name = "None";
|
|
if (this.upgrades && this.path1_tier > 0) p1Name = this.upgrades.path1[this.path1_tier - 1].name;
|
|
if (this.upgrades && this.path2_tier > 0) p2Name = this.upgrades.path2[this.path2_tier - 1].name;
|
|
|
|
var tact = TD.getTacticalMatchups(this);
|
|
var cannotStr = tact.cannot ? "\nCANNOT: " + tact.cannot : "";
|
|
|
|
var stats = "UNIT : " + this.name.toUpperCase() + "\n" +
|
|
"LEVEL : " + this.level + "\n" +
|
|
"XP : " + Math.floor(this.xp) + " / " + this.xp_needed + "\n" +
|
|
"DMG : " + this.damage + "\n" +
|
|
"RANGE : " + this.range.toFixed(1) + "\n" +
|
|
"KILLS : " + this.killed + "\n" +
|
|
"PATH 1: " + p1Name + "\n" +
|
|
"PATH 2: " + p2Name + "\n" +
|
|
"STRONG: " + tact.strong + "\n" +
|
|
"WEAK : " + tact.weak + cannotStr;
|
|
tooltip.innerText = stats;
|
|
|
|
tooltip.style.display = "block";
|
|
}
|
|
},
|
|
|
|
onOut: function () {
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (tooltip) tooltip.style.display = "none";
|
|
},
|
|
onClick: function () {
|
|
if (this.is_pre_building || this.scene.state != 1 || !this.map.is_main_map) return;
|
|
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (tooltip) tooltip.style.display = "none";
|
|
var overlay = document.getElementById("tower-modal-overlay");
|
|
if (!overlay) return;
|
|
|
|
// Initialize tracking arrays if they don't exist
|
|
if (typeof this.path1_tier === 'undefined') this.path1_tier = 0;
|
|
if (typeof this.path2_tier === 'undefined') this.path2_tier = 0;
|
|
|
|
document.getElementById("modal-tower-name").innerText = this.name.toUpperCase();
|
|
|
|
// NEW: Reset the Image to the default "T" text for normal towers
|
|
var imgContainer = document.getElementById("modal-tower-img");
|
|
if (imgContainer) {
|
|
imgContainer.innerHTML = "T";
|
|
}
|
|
|
|
// NEW: Reset the Targeting Dropdown back to standard Tower modes
|
|
var selTarget = document.getElementById("modal-target-mode");
|
|
if (selTarget) {
|
|
selTarget.innerHTML = '<option value="closest">Closest</option><option value="leader">Leader</option><option value="strongest">Strongest</option><option value="weakest">Weakest</option>';
|
|
selTarget.value = this.target_mode || "closest";
|
|
var _building = this;
|
|
selTarget.onchange = function() { _building.target_mode = this.value; };
|
|
}
|
|
|
|
|
|
// Determine Current Upgrades
|
|
var p1Name = "None"; var p2Name = "None";
|
|
if (this.upgrades && this.path1_tier > 0) p1Name = this.upgrades.path1[this.path1_tier - 1].name;
|
|
if (this.upgrades && this.path2_tier > 0) p2Name = this.upgrades.path2[this.path2_tier - 1].name;
|
|
|
|
var tact = TD.getTacticalMatchups(this);
|
|
var cannotHtml = tact.cannot ? "<b style='color:#ff5555'>Cannot target:</b> " + tact.cannot + "<br>" : "";
|
|
|
|
var statsHtml = "<b>Level:</b> " + this.level + "<br>" +
|
|
"<b>XP:</b> " + Math.floor(this.xp) + " / " + this.xp_needed + "<br><br>" +
|
|
"<b>Damage:</b> " + this.damage + "<br>" +
|
|
"<b>Range:</b> " + this.range.toFixed(1) + "<br>" +
|
|
"<b>Kills:</b> " + this.killed + "<br><br>" +
|
|
"<b style='color:#0f0'>Strong vs:</b> " + tact.strong + "<br>" +
|
|
"<b style='color:#f55'>Weak vs:</b> " + tact.weak + "<br>" +
|
|
cannotHtml + "<br>" +
|
|
"<b style='color:#ffcc00;'>Path 1:</b> " + p1Name + "<br>" +
|
|
"<b style='color:#ffcc00;'>Path 2:</b> " + p2Name;
|
|
document.getElementById("modal-tower-stats").innerHTML = statsHtml;
|
|
|
|
|
|
var _building = this;
|
|
var btnClose = document.getElementById("modal-close-btn");
|
|
var btnSell = document.getElementById("modal-btn-sell");
|
|
|
|
var newClose = btnClose.cloneNode(true); btnClose.parentNode.replaceChild(newClose, btnClose);
|
|
var newSell = btnSell.cloneNode(true); btnSell.parentNode.replaceChild(newSell, btnSell);
|
|
|
|
newClose.onclick = function() { overlay.style.display = "none"; };
|
|
|
|
// Update Sell Button text to show value
|
|
newSell.innerText = "SELL TOWER (" + _building.getSellMoney() + "c)";
|
|
newSell.onclick = function() {
|
|
_building.tryToSell();
|
|
overlay.style.display = "none";
|
|
};
|
|
|
|
overlay.onclick = function(e) { if(e.target === overlay) overlay.style.display = "none"; };
|
|
|
|
var selTarget = document.getElementById("modal-target-mode");
|
|
if (selTarget) {
|
|
selTarget.value = _building.target_mode || "closest";
|
|
selTarget.onchange = function() { _building.target_mode = this.value; };
|
|
}
|
|
|
|
// --- POPULATE UPGRADES ---
|
|
var btnPath1 = document.getElementById("modal-btn-path1");
|
|
var btnPath2 = document.getElementById("modal-btn-path2");
|
|
var newP1 = btnPath1.cloneNode(true); btnPath1.parentNode.replaceChild(newP1, btnPath1);
|
|
var newP2 = btnPath2.cloneNode(true); btnPath2.parentNode.replaceChild(newP2, btnPath2);
|
|
|
|
var drawIndicators = function(containerId, currentTier) {
|
|
var container = document.getElementById(containerId);
|
|
container.innerHTML = "";
|
|
for (var i = 0; i < 4; i++) {
|
|
var box = document.createElement("div");
|
|
box.style.width = "12px";
|
|
box.style.height = "12px";
|
|
box.style.border = "1px solid #0f0";
|
|
// Fill green if unlocked, empty if locked
|
|
box.style.background = (i < currentTier) ? "#0f0" : "transparent";
|
|
container.appendChild(box);
|
|
}
|
|
};
|
|
|
|
var updateUpgradeUI = function(pathNum, btnElement, currentTier, otherTier, pathData) {
|
|
var nameEl = btnElement.querySelector("#path" + pathNum + "-name");
|
|
var costEl = btnElement.querySelector("#path" + pathNum + "-cost");
|
|
var descEl = btnElement.querySelector("#path" + pathNum + "-desc");
|
|
|
|
drawIndicators("path" + pathNum + "-indicators", currentTier);
|
|
|
|
// Check Max Tier restriction (BTD5 logic: if one path is > 2, the other is capped at 2)
|
|
var isRestricted = (currentTier === 2 && otherTier > 2);
|
|
|
|
if (currentTier >= 4) {
|
|
nameEl.innerText = "MAX TIER REACHED";
|
|
costEl.innerText = "";
|
|
descEl.innerText = "No further upgrades available on this path.";
|
|
btnElement.disabled = true;
|
|
btnElement.style.backgroundColor = "#444";
|
|
btnElement.style.borderColor = "#555";
|
|
nameEl.style.color = "#888";
|
|
descEl.style.color = "#888";
|
|
btnElement.removeAttribute("data-cost");
|
|
} else if (isRestricted) {
|
|
nameEl.innerText = "PATH LOCKED";
|
|
costEl.innerText = "";
|
|
descEl.innerText = "Cannot upgrade past Tier 2 because the other path is Tier 3+.";
|
|
btnElement.disabled = true;
|
|
btnElement.style.backgroundColor = "#400";
|
|
btnElement.style.borderColor = "#800";
|
|
nameEl.style.color = "#888";
|
|
descEl.style.color = "#888";
|
|
btnElement.removeAttribute("data-cost");
|
|
} else {
|
|
var nextUpgrade = pathData[currentTier];
|
|
if (!nextUpgrade) return; // Failsafe
|
|
|
|
nameEl.innerText = nextUpgrade.name;
|
|
costEl.innerText = nextUpgrade.cost + "c";
|
|
descEl.innerText = nextUpgrade.desc;
|
|
|
|
// Charisma Perk Check
|
|
var actualCost = nextUpgrade.cost;
|
|
if (TD.perks && TD.perks.C) actualCost = Math.floor(actualCost * 0.85);
|
|
if (TD.perks && TD.perks.C) costEl.innerText = actualCost + "c"; // Show discounted cost
|
|
|
|
btnElement.setAttribute("data-cost", actualCost);
|
|
|
|
if (TD.money >= actualCost) {
|
|
btnElement.disabled = false;
|
|
btnElement.style.backgroundColor = "#2a7a2a";
|
|
btnElement.style.borderColor = "#0f0";
|
|
nameEl.style.color = "#000";
|
|
descEl.style.color = "#111";
|
|
} else {
|
|
btnElement.disabled = true;
|
|
btnElement.style.backgroundColor = "#444";
|
|
btnElement.style.borderColor = "#555";
|
|
nameEl.style.color = "#888";
|
|
descEl.style.color = "#888";
|
|
}
|
|
|
|
// ALWAYS assign the click handler! (It won't fire if disabled by the browser)
|
|
btnElement.onclick = function() {
|
|
if (TD.money >= actualCost) {
|
|
TD.money -= actualCost;
|
|
if (pathNum === 1) _building.path1_tier++;
|
|
else _building.path2_tier++;
|
|
|
|
// --- NEW: CALL THE CENTRALIZED UPGRADE HANDLER ---
|
|
if (nextUpgrade.effects && typeof TD.SpecialEffects !== 'undefined') {
|
|
TD.SpecialEffects.onApply(_building, nextUpgrade.effects);
|
|
}
|
|
|
|
// NEW: ANNOUNCE UPGRADE
|
|
if (window.IntelData && TD.Announcer) {
|
|
// If they just bought Tier 4 (which pushes the tracker to 4)
|
|
if (pathNum === 1 && _building.path1_tier >= 4 || pathNum === 2 && _building.path2_tier >= 4) {
|
|
TD.Announcer.add(window.IntelData.getEvent("tower_max", TD.current_faction, _building.name), 4);
|
|
} else {
|
|
TD.Announcer.add(window.IntelData.getEvent("tower_upgrade", TD.current_faction, _building.name), 3);
|
|
}
|
|
}
|
|
|
|
// Visual feedback
|
|
TD.Explode("upg-" + TD.lang.rndStr(), { cx: _building.cx, cy: _building.cy, r: 20 * _TD.retina, step_level: 1, render_level: 9, color: "#00ff00", scene: _building.map.scene, time: 0.3 });
|
|
|
|
// Re-render UI instantly
|
|
_building.onClick();
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
// Grab the upgrade data from the faction mapping
|
|
if (_building.upgrades) {
|
|
updateUpgradeUI(1, newP1, _building.path1_tier, _building.path2_tier, _building.upgrades.path1);
|
|
updateUpgradeUI(2, newP2, _building.path2_tier, _building.path1_tier, _building.upgrades.path2);
|
|
}
|
|
|
|
overlay.style.display = "flex";
|
|
}
|
|
};
|
|
TD.Building = function (id, cfg) { cfg.on_events = ["enter", "out", "click"]; var building = new TD.Element(id, cfg); TD.lang.mix(building, building_obj); building._init(cfg); return building; };
|
|
|
|
var bullet_obj = {
|
|
_init: function (cfg) {
|
|
cfg = cfg || {}; this.speed = cfg.speed; this.target = cfg.target; this.cx = cfg.x; this.cy = cfg.y;
|
|
|
|
if (typeof TD !== 'undefined') TD.total_shots = (TD.total_shots || 0) + 1; // Track total shots fired
|
|
|
|
// PSYCHO BUFF: 25% More Damage
|
|
|
|
var finalDmg = cfg.damage;
|
|
if (typeof TD !== 'undefined' && TD.psycho_active > 0) {
|
|
finalDmg = Math.floor(finalDmg * 1.50); // 50% More Damage!
|
|
|
|
}
|
|
this.damage = finalDmg;
|
|
|
|
this.r = cfg.r || Math.max(Math.log(this.damage), 2); if (this.r < 1) this.r = 1; if (this.r > 6) this.r = 6;
|
|
this.building = cfg.building || null; this.map = cfg.map || this.building.map; this.type = cfg.type || 1; this.color = cfg.color || "#000";
|
|
|
|
// NEW: Lifespan limit so bullets don't chase forever
|
|
// Range is in grid spaces (e.g. 5). Multiply by grid size to get max pixel travel distance,
|
|
// then divide by base speed to get the exact number of frames it should live.
|
|
var max_travel_px = (this.building.range * TD.grid_size) * 3; // INCREASED: Allow 300% travel distance to ensure hits
|
|
this.max_life = Math.floor(max_travel_px / (10 * this.speed)); // INCREASED base life floor
|
|
|
|
this.map.bullets.push(this); this.addToScene(this.map.scene, 1, 6);
|
|
|
|
if (this.type == 1) { this.caculate(); }
|
|
},
|
|
|
|
|
|
caculate: function () {
|
|
var sx, sy, c, tx = this.target.cx, ty = this.target.cy, speed;
|
|
sx = tx - this.cx; sy = ty - this.cy; c = Math.sqrt(Math.pow(sx, 2) + Math.pow(sy, 2)); speed = 20 * this.speed * TD.global_speed;
|
|
this.vx = sx * speed / c; this.vy = sy * speed / c;
|
|
|
|
// ANTI-AIR: 50% Damage Penalty if an LMG is shooting a Flyer
|
|
if (this.target.is_flying && this.building && (this.building.visual_type === "LMG" || this.building.type === "LMG")) {
|
|
this.damage = Math.floor(this.damage * 0.5);
|
|
if (this.damage < 1) this.damage = 1;
|
|
}
|
|
},
|
|
|
|
checkOutOfMap: function () {
|
|
this.is_valid = !(this.cx < this.map.x || this.cx > this.map.x2 || this.cy < this.map.y || this.cy > this.map.y2); return !this.is_valid;
|
|
},
|
|
checkHit: function () {
|
|
var cx = this.cx, cy = this.cy, r = this.r * _TD.retina, monster = this.map.anyMonster(function (obj) { return Math.pow(obj.cx - cx, 2) + Math.pow(obj.cy - cy, 2) <= Math.pow(obj.r + r, 2) * 2; });
|
|
if (monster) {
|
|
// NEW: Radius Damage (AoE) Logic
|
|
var bName = this.building.name.toUpperCase();
|
|
// Identify explosive ordnance (Slow cannons, explicitly named weapons, or upgraded Scrap Turret)
|
|
var is_aoe = (this.building.visual_type === "cannon" && (this.building.speed <= 1.5 || bName.indexOf("SCRAP TURRET") > -1)) ||
|
|
bName.indexOf("PLASMA") > -1 || bName.indexOf("MISSILE") > -1 || bName.indexOf("ARTILLERY") > -1;
|
|
|
|
if (is_aoe) {
|
|
var blast_radius = TD.grid_size * 1.5; // Default medium blast (1.5 tiles)
|
|
if (bName.indexOf("SCRAP TURRET") > -1) blast_radius = TD.grid_size * 0.8; // True Micro-explosions
|
|
if (this.building.speed <= 0.2) blast_radius = TD.grid_size * 3.0; // Massive Nuke/Artillery blast
|
|
|
|
// Spawn a massive fiery visual explosion
|
|
TD.Explode(this.id + "-explode", { cx: this.cx, cy: this.cy, r: blast_radius / _TD.retina, step_level: this.step_level, render_level: this.render_level, color: "#ff5500", scene: this.map.scene, time: 0.4 });
|
|
|
|
var _this = this;
|
|
TD.lang.each(this.map.monsters, function (m) {
|
|
if (m.is_valid) {
|
|
var dist = Math.sqrt(Math.pow(m.cx - cx, 2) + Math.pow(m.cy - cy, 2));
|
|
if (dist <= blast_radius) {
|
|
// Damage lowers outward from impact (minimum 25% damage at the absolute edge)
|
|
var dmg_percent = 1 - (dist / blast_radius);
|
|
if (dmg_percent < 0.25) dmg_percent = 0.25;
|
|
|
|
var splash_dmg = Math.floor(_this.damage * dmg_percent);
|
|
if (splash_dmg < 1) splash_dmg = 1; // Always deal at least 1 damage
|
|
|
|
m.beHit(_this.building, splash_dmg);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
// CRYO / SLOW EFFECT (0 Damage, AoE Splash)
|
|
if (this.building && this.building.is_cryo) {
|
|
var blast_radius = TD.grid_size * 1.5; // 1.5 tile splash radius
|
|
TD.Explode("slow-splash-" + TD.lang.rndStr(), { cx: this.cx, cy: this.cy, r: blast_radius / _TD.retina, step_level: 1, render_level: 9, color: "#aaffff", scene: this.map.scene, time: 0.4 });
|
|
|
|
var _this = this;
|
|
TD.lang.each(this.map.monsters, function (m) {
|
|
if (m.is_valid) {
|
|
var dist = Math.sqrt(Math.pow(m.cx - _this.cx, 2) + Math.pow(m.cy - _this.cy, 2));
|
|
if (dist <= blast_radius) {
|
|
m.is_slowed = true;
|
|
m.slow_timer = TD.exp_fps * 4; // Slowed for 4 seconds
|
|
}
|
|
}
|
|
});
|
|
} else if (this.building && this.building.is_poison) {
|
|
// POISON EFFECT (0 Base Damage, Heavy DoT)
|
|
monster.is_poisoned = true;
|
|
monster.poison_timer = TD.exp_fps * 5; // 5 seconds of poison
|
|
TD.Explode("poison-" + TD.lang.rndStr(), { cx: this.cx, cy: this.cy, r: this.r * 2, step_level: this.step_level, render_level: 9, color: "#00ff00", scene: this.map.scene, time: 0.3 });
|
|
} else {
|
|
// Standard Single-Target Hit (Bullets, Lasers)
|
|
monster.beHit(this.building, this.damage);
|
|
TD.Explode(this.id + "-explode", { cx: this.cx, cy: this.cy, r: this.r, step_level: this.step_level, render_level: this.render_level, color: this.color, scene: this.map.scene, time: 0.2 });
|
|
|
|
// --- UPGRADE EFFECTS ---
|
|
var should_destroy = true;
|
|
if (typeof TD.SpecialEffects !== 'undefined') {
|
|
should_destroy = TD.SpecialEffects.onHit(this, monster);
|
|
}
|
|
|
|
if (!should_destroy) {
|
|
return false; // Pierce active, keep flying
|
|
}
|
|
}
|
|
}
|
|
|
|
this.is_valid = false;
|
|
return true;
|
|
|
|
}
|
|
return false;
|
|
},
|
|
|
|
step: function () {
|
|
// NEW: Dynamic Range Limit. The bullet fizzles out if it flies too far from its parent tower.
|
|
if (this.building && this.building.is_valid) {
|
|
// Calculate distance from bullet to the center of the tower that fired it
|
|
var dist_from_tower = Math.sqrt(Math.pow(this.cx - this.building.cx, 2) + Math.pow(this.cy - this.building.cy, 2));
|
|
|
|
// Allow a 10% buffer so bullets hitting enemies perfectly on the edge of the range ring don't despawn prematurely
|
|
var max_allowed_distance = (this.building.range * TD.grid_size) * 1.1;
|
|
|
|
if (dist_from_tower > max_allowed_distance) {
|
|
this.is_valid = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// NEW: Home in on target every frame to prevent missing at high speeds!
|
|
if (this.target && this.target.is_valid && !this.target.is_fake) {
|
|
var dist_to_target = Math.sqrt(Math.pow(this.target.cx - this.cx, 2) + Math.pow(this.target.cy - this.cy, 2));
|
|
var speed_this_frame = 20 * this.speed * TD.global_speed; // Maximum distance bullet travels per frame
|
|
|
|
if (dist_to_target <= speed_this_frame) {
|
|
// Prevent overshoot by snapping directly to the target
|
|
this.cx = this.target.cx;
|
|
this.cy = this.target.cy;
|
|
this.vx = 0;
|
|
this.vy = 0;
|
|
} else {
|
|
this.caculate();
|
|
}
|
|
} else if (this.vx === 0 && this.vy === 0) {
|
|
// Failsafe: Target died exactly as bullet arrived
|
|
this.is_valid = false;
|
|
return;
|
|
}
|
|
|
|
|
|
if (this.checkOutOfMap() || this.checkHit()) return;
|
|
|
|
this.cx += this.vx; this.cy += this.vy;
|
|
},
|
|
|
|
|
|
render: function () {
|
|
var ctx = TD.ctx;
|
|
|
|
// FLAMER RENDER: Glowing, expanding circles without tails
|
|
if (this.cfg && this.cfg.is_flame) {
|
|
// Calculate distance from the tower so the flame can physically expand as it travels
|
|
var dist = Math.sqrt(Math.pow(this.cx - this.building.cx, 2) + Math.pow(this.cy - this.building.cy, 2));
|
|
var size = this.r + (dist * 0.08); // Expands outward
|
|
|
|
ctx.globalCompositeOperation = "lighter"; // Makes overlapping fire particles glow intensely
|
|
ctx.fillStyle = this.color;
|
|
ctx.shadowBlur = 15;
|
|
ctx.shadowColor = this.color;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(this.cx, this.cy, size, 0, Math.PI * 2, true);
|
|
ctx.fill();
|
|
|
|
// Cleanup context so other things don't accidentally glow
|
|
ctx.shadowBlur = 0;
|
|
ctx.globalCompositeOperation = "source-over";
|
|
return;
|
|
}
|
|
|
|
// STANDARD BULLET RENDER
|
|
// Normalize velocity to get a consistent direction
|
|
var norm = Math.sqrt(this.vx * this.vx + this.vy * this.vy) || 1;
|
|
var tailLen = 12 * _TD.retina; // Fixed, short tail length (12 pixels)
|
|
|
|
ctx.fillStyle = this.color;
|
|
ctx.strokeStyle = this.color;
|
|
ctx.lineWidth = Math.min(this.r, 2 * _TD.retina); // Keep the tail thin
|
|
ctx.lineCap = "round";
|
|
|
|
// Draw the head of the projectile
|
|
ctx.beginPath();
|
|
ctx.arc(this.cx, this.cy, this.r, 0, Math.PI * 2, true);
|
|
ctx.fill();
|
|
|
|
// Draw the fixed-length tail extending backwards
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.cx, this.cy);
|
|
ctx.lineTo(this.cx - (this.vx / norm) * tailLen, this.cy - (this.vy / norm) * tailLen);
|
|
ctx.stroke();
|
|
}
|
|
|
|
};
|
|
TD.Bullet = function (id, cfg) { var bullet = new TD.Element(id, cfg); TD.lang.mix(bullet, bullet_obj); bullet._init(cfg); return bullet; };
|
|
|
|
});
|
|
|
|
|
|
_TD.a.push(function (TD) {
|
|
// --- NEW: V.A.T.S. Initialization ---
|
|
TD.vats_charge = 0; // Starts at 0, max 100
|
|
TD.vats_active = false; // Is VATS currently running?
|
|
TD.vats_shots = 0; // Shots remaining
|
|
TD.vats_timer = 0; // 10 second countdown
|
|
TD.vats_max_charge = 5000; // Vastly increased to handle score integration
|
|
|
|
// NEW: Consumable Inventory & Cooldown Tracker
|
|
// Added faction-specific items
|
|
TD.inventory = { stimpak: 0, psycho: 0, cryomine: 0, artillery: 0, capmine: 0, ncr_artillery: 0, bos_laser: 0, mm_flare: 0, raider_nuka: 0 };
|
|
TD.cooldowns = { stimpak: 0, psycho: 0, cryomine: 0, artillery: 0, capmine: 0, ncr_artillery: 0, bos_laser: 0, mm_flare: 0, raider_nuka: 0 };
|
|
|
|
|
|
// Calculates the required cooldown frames based on difficulty
|
|
TD.getCooldownFrames = function() {
|
|
if (TD.difficulty < 1) return 1 * 24; // Halved
|
|
if (TD.difficulty > 1) return 12 * 24; // Halved
|
|
return 5 * 24; // Halved
|
|
};
|
|
|
|
|
|
// NEW: The core Cooldown loop (Runs every frame)
|
|
TD.stepConsumables = function() {
|
|
for (var key in TD.cooldowns) {
|
|
if (TD.cooldowns[key] > 0) {
|
|
TD.cooldowns[key]--;
|
|
var btn = document.getElementById("btn-item-" + key);
|
|
if (btn) {
|
|
// Calculate how much has RECHARGED (100% means fully ready)
|
|
var percentRecharged = 100 - ((TD.cooldowns[key] / TD.getCooldownFrames()) * 100);
|
|
|
|
// The button stays physically disabled
|
|
btn.disabled = true;
|
|
btn.style.cursor = "not-allowed";
|
|
|
|
// Draw the bright yellow light sliding in from the left
|
|
// #f2d57e is the var(--btn-on) color, #555 is the standard disabled background
|
|
btn.style.backgroundImage = "linear-gradient(to right, #f2d57e " + percentRecharged + "%, #555 " + percentRecharged + "%)";
|
|
|
|
// Keep text readable against the dark background while it fills
|
|
btn.style.color = "#222";
|
|
btn.style.textShadow = "none";
|
|
|
|
}
|
|
|
|
// When cooldown finishes, reset button if we have inventory
|
|
if (TD.cooldowns[key] <= 0) {
|
|
var btnReset = document.getElementById("btn-item-" + key);
|
|
if (btnReset) btnReset.style.backgroundImage = "none";
|
|
TD.updateItemButton(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
// SMART CIRCUIT: Auto-disable Stimpak if HP is 100
|
|
if (TD.inventory.stimpak > 0 && TD.cooldowns.stimpak <= 0) {
|
|
var stimBtn = document.getElementById("btn-item-stimpak");
|
|
if (stimBtn) {
|
|
// If Siege mode, check base_health, otherwise check life
|
|
var isSiege = TD.stage && TD.stage.current_act && TD.stage.current_act.current_scene && TD.stage.current_act.current_scene.map && TD.stage.current_act.current_scene.map.map_type === "siege";
|
|
var isFullHealth = isSiege ? (TD.base_health >= 1000) : (TD.life >= 100);
|
|
|
|
if (isFullHealth && !stimBtn.disabled) {
|
|
stimBtn.disabled = true; // Cut power
|
|
stimBtn.removeAttribute("data-warmed-up"); // Erase memory so it sparks when damaged
|
|
} else if (!isFullHealth && stimBtn.disabled) {
|
|
// We took damage! Restore power natively and let the Observer catch it for the RNG spark!
|
|
stimBtn.disabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Psycho Active Buff Timer
|
|
if (TD.psycho_active > 0) {
|
|
TD.psycho_active--;
|
|
if (TD.psycho_active <= 0) {
|
|
TD.log("PSYCHO BUFF WORE OFF");
|
|
document.getElementById("td-board").style.boxShadow = "none"; // Remove red screen glow
|
|
}
|
|
}
|
|
|
|
// --- S.P.E.C.I.A.L. Overdrive Timers ---
|
|
if (TD.overdrive && TD.overdrive_cd && TD.updateSpecialButtons) {
|
|
for (var key in TD.overdrive) {
|
|
if (TD.overdrive[key] > 0) {
|
|
TD.overdrive[key]--;
|
|
if (TD.overdrive[key] <= 0 && key === 'P' && TD.stage && TD.stage.current_act) {
|
|
// Remove infinite range snipers when Perception ends
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
if (map) { TD.lang.each(map.buildings, function(b) { b.target = null; }); }
|
|
}
|
|
} else if (TD.overdrive_cd[key] > 0) {
|
|
TD.overdrive_cd[key]--;
|
|
}
|
|
}
|
|
if (TD.iframe % 6 === 0) TD.updateSpecialButtons(); // Update UI every 1/4 second
|
|
}
|
|
};
|
|
|
|
|
|
|
|
// NEW: Function to update button text & toggle disabled state
|
|
TD.updateItemButton = function(type) {
|
|
var counts = TD.inventory[type];
|
|
var btn = document.getElementById("btn-item-" + type);
|
|
if (btn) {
|
|
var name = type.toUpperCase();
|
|
if (type === "cryomine") name = "CRYO MINE";
|
|
if (type === "capmine") name = "CAP MINE";
|
|
if (type === "ncr_artillery") name = "BARRAGE";
|
|
if (type === "bos_laser") name = "ORBITAL";
|
|
if (type === "mm_flare") name = "MILITIA";
|
|
if (type === "raider_nuka") name = "NUKA GRENADE";
|
|
|
|
var wasDisabled = btn.disabled;
|
|
|
|
|
|
// CLEAR the visual cooldown styles explicitly so CSS takes over
|
|
btn.style.background = "";
|
|
btn.style.opacity = "";
|
|
btn.style.cursor = "";
|
|
|
|
if (counts > 0) {
|
|
btn.innerText = name + " (" + counts + ")";
|
|
btn.style.color = ""; // Let CSS handle color
|
|
btn.disabled = false; // Enable button!
|
|
|
|
// If it just became ready (off cooldown/gained inventory), flicker it on!
|
|
if (wasDisabled) {
|
|
btn.className = btn.className.replace(/warm-up-\d/g, "").trim();
|
|
void btn.offsetWidth; // Force CSS reflow
|
|
btn.classList.add("warm-up-" + (Math.floor(Math.random() * 5) + 1));
|
|
}
|
|
} else {
|
|
btn.innerText = name;
|
|
btn.style.color = "";
|
|
btn.disabled = true; // Disable if empty
|
|
btn.style.opacity = "";
|
|
btn.style.cursor = "";
|
|
|
|
// Strip glow immediately if empty
|
|
btn.className = btn.className.replace(/warm-up-\d/g, "").trim();
|
|
}
|
|
}
|
|
};
|
|
|
|
// --- NEW: S.P.E.C.I.A.L. BUTTON UI LOGIC ---
|
|
TD.updateSpecialButtons = function() {
|
|
var letters = ['S', 'P', 'E', 'C', 'I', 'A', 'L'];
|
|
for (var i = 0; i < letters.length; i++) {
|
|
var letter = letters[i];
|
|
var btn = document.getElementById("btn-special-" + letter.toLowerCase());
|
|
if (!btn) continue;
|
|
|
|
if (TD.perks && TD.perks[letter]) {
|
|
// Button is owned! Check cooldowns.
|
|
if (TD.overdrive_cd[letter] > 0) {
|
|
btn.disabled = true;
|
|
var percent = 100 - ((TD.overdrive_cd[letter] / (TD.exp_fps * 120)) * 100); // 2 min cooldown
|
|
btn.style.backgroundImage = "linear-gradient(to top, #f2d57e " + percent + "%, #333 " + percent + "%)";
|
|
btn.style.color = "#888";
|
|
} else if (TD.overdrive[letter] > 0) {
|
|
btn.disabled = true; // Actively running!
|
|
btn.style.backgroundImage = "none";
|
|
btn.style.backgroundColor = "#ffcc00"; // Solid yellow
|
|
btn.style.color = "#000";
|
|
btn.style.boxShadow = "0 0 15px #ffcc00";
|
|
} else {
|
|
btn.disabled = false; // Ready to use!
|
|
btn.style.backgroundImage = "none";
|
|
btn.style.backgroundColor = "#2a7a2a"; // Ready Green
|
|
btn.style.color = "#fff";
|
|
btn.style.boxShadow = "none";
|
|
}
|
|
} else {
|
|
btn.disabled = true; // Perk not owned
|
|
btn.style.background = "#222";
|
|
btn.style.color = "#444";
|
|
btn.style.borderColor = "#111";
|
|
}
|
|
}
|
|
};
|
|
|
|
// Wire up the click events for the 7 buttons
|
|
var letters = ['S', 'P', 'E', 'C', 'I', 'A', 'L'];
|
|
letters.forEach(function(letter) {
|
|
var btn = document.getElementById("btn-special-" + letter.toLowerCase());
|
|
if (btn) {
|
|
btn.onclick = function() {
|
|
if (TD.perks && TD.perks[letter] && TD.overdrive_cd[letter] <= 0 && TD.overdrive[letter] <= 0) {
|
|
TD.overdrive[letter] = TD.exp_fps * 15; // 15 Seconds of Overdrive
|
|
TD.overdrive_cd[letter] = TD.exp_fps * 135; // 2 minute cooldown starts after
|
|
|
|
var desc = document.getElementById("ui-action-desc");
|
|
if (desc) desc.innerText = letter + " OVERDRIVE ENGAGED!";
|
|
|
|
// Instant Trigger Effects (Charisma Drops, Luck Explosions, Intel XP)
|
|
if (letter === 'C') {
|
|
// CARAVAN DROP (Coins from sky)
|
|
for (var i = 0; i < 15; i++) {
|
|
(function(delay) {
|
|
setTimeout(function() {
|
|
var loot = new TD.Loot("loot-" + TD.lang.rndStr(), { cx: TD.stage.width/2 + (Math.random()*400-200), cy: TD.padding, map: _this.map });
|
|
loot.loot_type = "money"; loot.value = 50; loot.altitude = 400; // Drops from 400px high
|
|
loot.vx = (Math.random()*4)-2; loot.vy = 2; // Falls downward
|
|
}, delay * 150);
|
|
})(i);
|
|
}
|
|
} else if (letter === 'I') {
|
|
// INSTANT XP BURST
|
|
if (_this.map) {
|
|
TD.lang.each(_this.map.buildings, function(b) { if (b.is_weapon) b.gainXp(100); }); // Flat 100 XP to all towers
|
|
}
|
|
} else if (letter === 'A') {
|
|
// INSTANT COOLDOWN RESET
|
|
for (var k in TD.cooldowns) TD.cooldowns[k] = 0;
|
|
} else if (letter === 'L') {
|
|
// LUCK EXPLOSION
|
|
for (var i = 0; i < 5; i++) {
|
|
var loot = new TD.Loot("loot-" + TD.lang.rndStr(), { cx: TD.stage.width/2 + (Math.random()*600-300), cy: TD.stage.height/2 + (Math.random()*400-200), map: _this.map });
|
|
loot.loot_type = "lunchbox";
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
// NEW: Loot Drop Object
|
|
|
|
TD.Loot = function (id, cfg) {
|
|
|
|
cfg.on_events = ["click", "enter", "out"]; // Built-in engine click listener!
|
|
var loot = new TD.Element(id, cfg);
|
|
|
|
loot._init = function(cfg) {
|
|
|
|
this.cx = cfg.cx; this.cy = cfg.cy; this.map = cfg.map; this.is_valid = true;
|
|
|
|
// NEW: Smart Off-Path Targeting (5x5 Grid Radius)
|
|
var currentGridX = Math.floor((this.cx - this.map.x) / TD.grid_size);
|
|
var currentGridY = Math.floor((this.cy - this.map.y) / TD.grid_size);
|
|
|
|
var validTargets = [];
|
|
var maxRadius = Math.max(this.map.grid_x, this.map.grid_y); // Search up to full map size
|
|
|
|
// Expanding ring search to guarantee finding the closest off-path tile
|
|
for (var r = 1; r <= maxRadius; r++) {
|
|
for (var dx = -r; dx <= r; dx++) {
|
|
for (var dy = -r; dy <= r; dy++) {
|
|
if (Math.abs(dx) !== r && Math.abs(dy) !== r) continue; // Only check perimeter of the current ring
|
|
|
|
var targetX = currentGridX + dx;
|
|
var targetY = currentGridY + dy;
|
|
|
|
// Strict map boundaries to prevent wrapping
|
|
if (targetX < 0 || targetX >= this.map.grid_x || targetY < 0 || targetY >= this.map.grid_y) continue;
|
|
|
|
var g = this.map.getGrid(targetX, targetY);
|
|
|
|
// Add to targets if there is NO tower (build_flag !== 2) AND it's not a dedicated path
|
|
if (g && g.build_flag !== 2 && (g.passable_flag === 0 || g.build_flag === 1)) {
|
|
validTargets.push(g);
|
|
}
|
|
}
|
|
}
|
|
if (validTargets.length > 0) break; // Stop expanding as soon as we find the closest valid tile!
|
|
}
|
|
|
|
var tcx = this.cx;
|
|
var tcy = this.cy;
|
|
|
|
if (validTargets.length > 0) {
|
|
// Throw to a random valid off-path tile in the closest available ring
|
|
var targetGrid = validTargets[Math.floor(Math.random() * validTargets.length)];
|
|
tcx = targetGrid.cx + (Math.random() * 20 - 10);
|
|
tcy = targetGrid.cy + (Math.random() * 20 - 10);
|
|
} else {
|
|
// Absolute last resort fallback (e.g. entire map is completely filled)
|
|
tcx = this.cx + (Math.random() * 30 - 15);
|
|
tcy = this.cy + (Math.random() * 30 - 15);
|
|
}
|
|
|
|
this.target_cx = tcx;
|
|
this.target_cy = tcy;
|
|
|
|
this.fling_timer = 20; // Reaches target in 20 frames
|
|
this.vx = (this.target_cx - this.cx) / this.fling_timer;
|
|
this.vy = (this.target_cy - this.cy) / this.fling_timer;
|
|
|
|
this.altitude = 0; // Starts on the ground
|
|
|
|
var r = TD.grid_size * 0.4;
|
|
this.x = this.cx - r; this.y = this.cy - r; this.width = r * 2; this.height = r * 2;
|
|
this.x2 = this.x + this.width; this.y2 = this.y + this.height;
|
|
|
|
if (!this.map.loots) this.map.loots = [];
|
|
this.map.loots.push(this);
|
|
|
|
// Announce that Loot has dropped!
|
|
if (window.IntelData && TD.Announcer) TD.Announcer.add(window.IntelData.getEvent("loot_drop", TD.current_faction), 3);
|
|
|
|
var duration = 20;
|
|
|
|
|
|
if (TD.difficulty < 1) duration = 35;
|
|
else if (TD.difficulty > 1) duration = 12;
|
|
this.life = duration * 24;
|
|
this.max_life = this.life;
|
|
|
|
var base_bounty = cfg.monster_bounty || 10;
|
|
this.value = Math.floor(base_bounty * (0.5 + Math.random()));
|
|
|
|
// --- NEW: Handle Pre-defined Loot (from Lunchboxes) vs RNG ---
|
|
if (cfg.forced_type) {
|
|
this.loot_type = cfg.forced_type;
|
|
this.value = cfg.forced_value || this.value;
|
|
} else if (window.devForcedDrop && window.devForcedDrop !== "none") {
|
|
// --- Dev Cheat Override ---
|
|
if (window.devForcedDrop === "faction") {
|
|
var factionLoot = { "NCR": "ncr_artillery", "BOS": "bos_laser", "MINUTEMEN": "mm_flare", "RAIDERS": "raider_nuka" };
|
|
this.loot_type = factionLoot[TD.current_faction] || "artillery";
|
|
} else {
|
|
this.loot_type = window.devForcedDrop;
|
|
}
|
|
window.devForcedDrop = "none";
|
|
var d = document.getElementById("ui-intel-brief");
|
|
if (d) d.innerText = "FORCED DROP ACQUIRED.";
|
|
} else {
|
|
// Standard RNG
|
|
var roll = Math.random();
|
|
if (roll < 0.70) this.loot_type = "money";
|
|
else if (roll < 0.75) this.loot_type = "lunchbox";
|
|
else if (roll < 0.80) this.loot_type = "stimpak";
|
|
else if (roll < 0.85) this.loot_type = "psycho";
|
|
else if (roll < 0.90) this.loot_type = "cryomine";
|
|
else if (roll < 0.95) this.loot_type = "capmine";
|
|
else {
|
|
var factionLoot = { "NCR": "ncr_artillery", "BOS": "bos_laser", "MINUTEMEN": "mm_flare", "RAIDERS": "raider_nuka" };
|
|
this.loot_type = factionLoot[TD.current_faction] || "artillery";
|
|
}
|
|
}
|
|
|
|
|
|
this.addToScene(this.map.scene, 1, 9);
|
|
|
|
|
|
};
|
|
|
|
loot.step = function () {
|
|
if (!this.is_valid) return;
|
|
|
|
// --- NEW: Magnet Flight to UI ---
|
|
if (this.is_collecting) {
|
|
this.cx += this.vx;
|
|
this.cy += this.vy;
|
|
this.altitude *= 0.8; // Drop altitude fast so it centers on UI
|
|
|
|
// Keep hitbox updated
|
|
var r = TD.grid_size * 0.4;
|
|
this.x = this.cx - r; this.y = this.cy - r;
|
|
this.x2 = this.x + (r*2); this.y2 = this.y + (r*2);
|
|
|
|
this.collect_timer--;
|
|
if (this.collect_timer <= 0) {
|
|
this.giveReward();
|
|
this.is_valid = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
// Apply physics throw if still flinging
|
|
if (this.fling_timer > 0) {
|
|
this.cx += this.vx;
|
|
this.cy += this.vy;
|
|
|
|
// Sine wave formula for jumping: Starts at 0, peaks at middle of timer, ends at 0
|
|
// Math.sin(0 to PI) creates a perfect arc.
|
|
var progress = 1 - (this.fling_timer / 20); // 0.0 to 1.0
|
|
this.altitude = Math.sin(progress * Math.PI) * 40; // Jump up to 40 pixels high
|
|
|
|
// Keep hitbox updated as it flies
|
|
var r = TD.grid_size * 0.4;
|
|
this.x = this.cx - r; this.y = this.cy - r;
|
|
this.x2 = this.x + (r*2); this.y2 = this.y + (r*2);
|
|
|
|
this.fling_timer--;
|
|
} else {
|
|
this.altitude = 0; // Rest flat on the ground
|
|
}
|
|
|
|
this.life--;
|
|
if (this.life <= 0) {
|
|
this.is_valid = false;
|
|
// Announce that we lost the loot!
|
|
if (window.IntelData && TD.Announcer) TD.Announcer.add(window.IntelData.getEvent("loot_lost", TD.current_faction), 4);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
loot.render = function () {
|
|
if (!this.is_valid) return;
|
|
var ctx = TD.ctx;
|
|
var size = (this.width / 2) * (this.is_collecting ? 1 : (this.life / this.max_life));
|
|
if (size < 2) size = 2;
|
|
|
|
// --- NEW: IMAGE RENDER (Lunchbox, Caps, Stimpak, Psycho) ---
|
|
if (this.loot_type === "lunchbox" || this.loot_type === "money" || this.loot_type === "stimpak" || this.loot_type === "psycho") {
|
|
if (!this.img) {
|
|
this.img = new Image();
|
|
if (this.loot_type === "lunchbox") this.img.src = "assets/images/lunchbox.png";
|
|
else if (this.loot_type === "stimpak") this.img.src = "assets/images/stimpack.png";
|
|
else if (this.loot_type === "psycho") this.img.src = "assets/images/psycho.png";
|
|
else this.img.src = "assets/images/caps.png";
|
|
}
|
|
if (this.img.complete) {
|
|
var imgSize = size * 2.5;
|
|
ctx.shadowBlur = 15;
|
|
ctx.shadowColor = "rgba(255, 255, 255, 0.7)";
|
|
|
|
|
|
|
|
ctx.drawImage(this.img, this.cx - (imgSize/2), this.cy - (imgSize/2) - this.altitude, imgSize, imgSize);
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
// --- STANDARD CIRCLE RENDER (For other items) ---
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(this.cx, this.cy - this.altitude, size, 0, Math.PI * 2, true);
|
|
|
|
ctx.shadowBlur = 10;
|
|
ctx.shadowColor = "rgba(255, 255, 255, 0.7)";
|
|
|
|
if (this.loot_type === "psycho") {
|
|
ctx.fillStyle = "#55ff55"; ctx.strokeStyle = "#00aa00";
|
|
} else if (this.loot_type === "cryomine") {
|
|
ctx.fillStyle = "#aaffff"; ctx.strokeStyle = "#00aaaa";
|
|
} else if (this.loot_type === "artillery") {
|
|
ctx.fillStyle = "#cccccc"; ctx.strokeStyle = "#555555";
|
|
} else {
|
|
// Faction Ultimate Drops
|
|
ctx.fillStyle = "#ffffaa"; ctx.strokeStyle = "#ffffff";
|
|
}
|
|
|
|
|
|
ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.shadowBlur = 0;
|
|
|
|
ctx.fillStyle = "#000"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
ctx.font = "bold " + Math.floor(size) + "px Arial";
|
|
var isUltimate = ["ncr_artillery", "bos_laser", "mm_flare", "raider_nuka"].indexOf(this.loot_type) > -1;
|
|
var letter = isUltimate ? "⭐" : this.loot_type.charAt(0).toUpperCase();
|
|
ctx.fillText(letter, this.cx, this.cy - this.altitude);
|
|
};
|
|
|
|
|
|
loot.onClick = function () {
|
|
if (!this.is_valid || this.is_collecting) return;
|
|
|
|
// Force hide the tooltip immediately, because this object is about to be deleted or moved!
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (tooltip) tooltip.style.display = "none";
|
|
|
|
// --- NEW: Lunchbox Physical Spitting Logic ---
|
|
if (this.loot_type === "lunchbox") {
|
|
|
|
var itemsToSpit = Math.floor(Math.random() * 3) + 1;
|
|
var possibleLoot = ["stimpak", "psycho", "cryomine", "capmine"];
|
|
var _map = this.map;
|
|
var _cx = this.cx;
|
|
var _cy = this.cy;
|
|
|
|
for (var i = 0; i < itemsToSpit; i++) {
|
|
(function(delay) {
|
|
setTimeout(function() {
|
|
var roll = Math.random();
|
|
var spawnType = "money";
|
|
var spawnVal = 10;
|
|
|
|
if (roll < 0.4) {
|
|
// Caps
|
|
spawnVal = Math.floor(Math.random() * 90) + 10;
|
|
} else {
|
|
// Consumable
|
|
spawnType = possibleLoot[Math.floor(Math.random() * possibleLoot.length)];
|
|
}
|
|
|
|
// Spawn a brand new physical loot object directly on the lunchbox coordinates
|
|
// It will naturally run its own `_init` and physics jump outward!
|
|
new TD.Loot("loot-" + TD.lang.rndStr(), {
|
|
cx: _cx,
|
|
cy: _cy,
|
|
map: _map,
|
|
forced_type: spawnType,
|
|
forced_value: spawnVal
|
|
});
|
|
|
|
}, delay * 200); // Stagger the jumps by 200ms
|
|
})(i);
|
|
}
|
|
|
|
// Explode effect to hide the lunchbox disappearing
|
|
TD.Explode("lb-pop-" + TD.lang.rndStr(), { cx: this.cx, cy: this.cy, r: 20, step_level: 1, render_level: 9, color: "#ffffff", scene: this.map.scene, time: 0.3 });
|
|
this.is_valid = false;
|
|
return;
|
|
}
|
|
|
|
// --- ALL OTHER LOOT: HTML DOM Flight Animation ---
|
|
var targetId = (this.loot_type === "money") ? "ui-money-container" : "btn-item-" + this.loot_type;
|
|
var targetEl = document.getElementById(targetId);
|
|
if (!targetEl) targetEl = document.getElementById("ui-money-container"); // Safe fallback
|
|
|
|
// 1. Get the physical screen coordinates of the loot right now
|
|
var crect = TD.canvas.getBoundingClientRect();
|
|
var scaleX = crect.width / TD.canvas.width;
|
|
var scaleY = crect.height / TD.canvas.height;
|
|
var startX = crect.left + (this.cx * scaleX);
|
|
var startY = crect.top + (this.cy * scaleY);
|
|
|
|
// 2. Create the temporary HTML image
|
|
var animImg = document.createElement("img");
|
|
|
|
// --- NEW: Correctly assign images based on loot type ---
|
|
if (this.loot_type === "stimpak") {
|
|
animImg.src = "assets/images/stimpack.png";
|
|
} else if (this.loot_type === "psycho") {
|
|
animImg.src = "assets/images/psycho.png";
|
|
} else if (this.loot_type === "money") {
|
|
animImg.src = "assets/images/caps.png";
|
|
} else {
|
|
// For Mines and Artillery, fallback to caps shape but keep their specific colored glow
|
|
animImg.src = "assets/images/caps.png";
|
|
}
|
|
|
|
|
|
animImg.style.position = "fixed";
|
|
animImg.style.left = startX + "px";
|
|
animImg.style.top = startY + "px";
|
|
animImg.style.width = "40px";
|
|
animImg.style.height = "40px";
|
|
animImg.style.zIndex = "999999";
|
|
animImg.style.pointerEvents = "none";
|
|
animImg.style.transition = "all 0.5s ease-in-out";
|
|
|
|
|
|
// Apply consistent off-white glow
|
|
animImg.style.filter = "drop-shadow(0px 0px 10px rgba(255, 255, 255, 0.7))";
|
|
|
|
|
|
document.body.appendChild(animImg);
|
|
|
|
// 3. Force reflow, then apply target coordinates to trigger CSS transition
|
|
void animImg.offsetWidth;
|
|
|
|
var targetRect = targetEl.getBoundingClientRect();
|
|
animImg.style.left = (targetRect.left + (targetRect.width / 2) - 20) + "px";
|
|
animImg.style.top = (targetRect.top + (targetRect.height / 2) - 20) + "px";
|
|
animImg.style.transform = "scale(0.5)"; // Shrink as it flies
|
|
animImg.style.opacity = "0.2";
|
|
|
|
// 4. Grant the reward and pop the text instantly!
|
|
this.giveReward();
|
|
|
|
// 5. Wait for CSS transition to finish, then delete the flying image
|
|
setTimeout(function() {
|
|
if (animImg.parentNode) animImg.parentNode.removeChild(animImg);
|
|
}, 500); // 500ms matches the CSS transition time
|
|
|
|
// Kill the canvas object immediately so the player can't double-click it
|
|
this.is_valid = false;
|
|
};
|
|
|
|
|
|
loot.giveReward = function () {
|
|
var msg = "";
|
|
var color = "#fff";
|
|
var _val = this.value;
|
|
var _type = this.loot_type;
|
|
|
|
if (_type === "money") {
|
|
// Do not award immediately! Wait 500ms for the animation to hit the UI.
|
|
setTimeout(function() {
|
|
TD.money += _val;
|
|
}, 500);
|
|
|
|
msg = "+$" + _val;
|
|
color = "#ffdd33";
|
|
} else {
|
|
if (TD.inventory[_type] < 5) {
|
|
// Delay inventory addition
|
|
setTimeout(function() {
|
|
TD.inventory[_type]++;
|
|
TD.updateItemButton(_type);
|
|
}, 500);
|
|
|
|
var name = _type.toUpperCase();
|
|
if (_type === "cryomine") name = "CRYO MINE";
|
|
if (_type === "capmine") name = "CAP MINE";
|
|
msg = "+1 " + name;
|
|
|
|
if (_type === "stimpak") color = "#ff5555";
|
|
else if (_type === "psycho") color = "#55ff55";
|
|
else if (_type === "cryomine") color = "#aaffff";
|
|
else if (_type === "capmine") color = "#ffaa00";
|
|
else if (_type === "artillery") color = "#cccccc";
|
|
else color = "#ffffff";
|
|
|
|
} else {
|
|
// Inventory full, refund caps (delayed)
|
|
setTimeout(function() {
|
|
TD.money += _val;
|
|
}, 500);
|
|
msg = "FULL! +$" + _val;
|
|
color = "#ffdd33";
|
|
}
|
|
}
|
|
|
|
// Spawn floating text IMMEDIATELY at the pickup location
|
|
new TD.FloatingText("ft-" + TD.lang.rndStr(), {
|
|
cx: this.x + (this.width / 2),
|
|
cy: this.y + (this.height / 2) - 20,
|
|
text: msg, color: color, map: this.map
|
|
});
|
|
|
|
// Announce we secured it! (Priority 2, it's good but not critical)
|
|
if (window.IntelData && TD.Announcer) TD.Announcer.add(window.IntelData.getEvent("consumable_used", TD.current_faction), 2);
|
|
};
|
|
|
|
|
|
// --- NEW: LORE TOOLTIPS FOR LOOT ---
|
|
loot.onEnter = function() {
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (!tooltip) return;
|
|
|
|
var title = "";
|
|
var desc = "";
|
|
|
|
if (this.loot_type === "money") {
|
|
title = "BOTTLECAPS";
|
|
desc = "Effect: +Caps.\nDon't ask why mutated bugs are carrying pre-war soda caps. Just take 'em.";
|
|
} else if (this.loot_type === "lunchbox") {
|
|
title = "SCAVENGED LUNCHBOX";
|
|
desc = "Effect: Drops random items.\nOnce held a Vault Boy thermos. Now holds whatever the last poor guy stuffed in it.";
|
|
} else if (this.loot_type === "stimpak") {
|
|
title = "STANDARD STIMPAK";
|
|
desc = "Effect: Restores 15 HP.\nIt's mostly just Med-X and Brahmin blood, but it keeps the defenses from collapsing.";
|
|
} else if (this.loot_type === "psycho") {
|
|
title = "DIRTY PSYCHO";
|
|
desc = "Effect: Boosts DMG/Speed by 25% for 10s.\nInject this into the targeting relays and watch the lead fly. Go get 'em!";
|
|
} else if (this.loot_type === "cryomine") {
|
|
title = "CRYO-GRENADE";
|
|
desc = "Effect: Freezes Enemies in Radius.\nLiquid nitrogen payload. Stops charging Deathclaws dead in their tracks.";
|
|
} else if (this.loot_type === "capmine") {
|
|
title = "SHRAPNEL CAP MINE";
|
|
desc = "Effect: Massive AoE Damage + Drops Caps.\nTurns pocket change into high-velocity projectiles. Pick up the caps after the mist clears.";
|
|
} else {
|
|
title = "FACTION SUPPLY DROP";
|
|
desc = "Effect: Grants Faction Ultimate.\nPre-war tech salvaged for the current war.";
|
|
}
|
|
|
|
tooltip.innerHTML = "<strong style='color:#f2d57e; font-size:14px;'>" + title + "</strong><br><br><span style='color:#ccc; font-size:12px;'>" + desc.replace(/\n/g, "<br>") + "</span>";
|
|
tooltip.style.display = "block";
|
|
};
|
|
|
|
loot.onOut = function() {
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (tooltip) tooltip.style.display = "none";
|
|
};
|
|
|
|
loot._init(cfg);
|
|
return loot;
|
|
};
|
|
|
|
|
|
|
|
// NEW: Mr. Handy Loot Collector Object
|
|
TD.MrHandy = function (id, cfg) {
|
|
cfg.on_events = ["click", "enter", "out"]; // NEW: Make him clickable!
|
|
var handy = new TD.Element(id, cfg);
|
|
|
|
handy._init = function(cfg) {
|
|
this.map = cfg.map;
|
|
|
|
// NEW: Upgrade and Behavior Tracking
|
|
this.name = "Mr. Handy";
|
|
this.level = 1;
|
|
this.path1_tier = 0;
|
|
this.path2_tier = 0;
|
|
this.behavior = "normal"; // normal, defensive, aggressive
|
|
this.is_weapon = false; // Starts as non-combat
|
|
|
|
this.upgrades = {
|
|
path1: [
|
|
{ name: "Circular Saw", cost: 250, desc: "Deals melee damage. Unlocks 'Aggressive' behavior." },
|
|
{ name: "Laser Emitter", cost: 600, desc: "Fires a basic laser at nearby enemies while moving." },
|
|
{ name: "Flamer Jet", cost: 1200, desc: "Sprays short-range AoE fire that leaves a burn effect." },
|
|
{ name: "Military Grade", cost: 3000, desc: "Converts to a heavily armored combat bot. High damage boost." }
|
|
],
|
|
path2: [
|
|
{ name: "Reinforced Plating", cost: 300, desc: "Triples his maximum HP." },
|
|
{ name: "Thruster Overclock", cost: 700, desc: "+50% movement speed to cross the map faster." },
|
|
{ name: "Magnetic Coil", cost: 1500, desc: "Automatically sucks in loot from 1.5 squares away." },
|
|
{ name: "Scavenger Protocol", cost: 2500, desc: "Randomly 'finds' and generates loot while exploring." }
|
|
]
|
|
};
|
|
|
|
|
|
// Start at provided click location, otherwise fallback to exit/center
|
|
if (cfg.cx != null && cfg.cy != null) {
|
|
|
|
this.cx = cfg.cx; this.cy = cfg.cy;
|
|
} else {
|
|
var startGrid = this.map.exit || this.map.getGrid(Math.floor(this.map.grid_x/2), Math.floor(this.map.grid_y/2));
|
|
this.cx = startGrid.cx; this.cy = startGrid.cy;
|
|
}
|
|
|
|
this.r = (TD.grid_size * 0.9) / 2; // 90% of grid size
|
|
this.width = this.r * 2; this.height = this.r * 2; // FIXED: Canvas needs exact dimensions to render the image
|
|
this.caculatePos();
|
|
|
|
this.is_valid = true;
|
|
this.max_life = 500; // Fairly tanky
|
|
this.life = this.max_life;
|
|
this.base_speed = 10;
|
|
this.loot_speed = this.base_speed * 2.0; // 100% faster sprint
|
|
|
|
this.target_loot = null;
|
|
this.wander_target = null;
|
|
this.wander_timer = 0;
|
|
|
|
this.img = new Image();
|
|
this.img.src = "assets/images/mrhandy.png";
|
|
|
|
this.addToScene(this.map.scene, 1, 9); // Render level 9 so he floats above towers
|
|
};
|
|
|
|
handy.caculatePos = function() {
|
|
this.x = this.cx - this.r; this.y = this.cy - this.r;
|
|
this.x2 = this.cx + this.r; this.y2 = this.cy + this.r;
|
|
};
|
|
|
|
handy.step = function() {
|
|
if (!this.is_valid) return;
|
|
|
|
var _this = this;
|
|
var current_speed = this.base_speed * TD.global_speed;
|
|
|
|
// 1. MAGNETIC COIL (Tier 3 Utility)
|
|
if (this.magnetic_coil) {
|
|
if (this.map.loots && this.map.loots.length > 0) {
|
|
for (var i = 0; i < this.map.loots.length; i++) {
|
|
var l = this.map.loots[i];
|
|
if (l.is_valid && !l.is_collecting && l.fling_timer <= 0) {
|
|
var d = Math.sqrt(Math.pow(l.cx - this.cx, 2) + Math.pow(l.cy - this.cy, 2));
|
|
if (d <= TD.grid_size * 1.5) { // 1.5 square pull radius
|
|
l.onClick(); // Instantly loot it!
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. SCAVENGER PROTOCOL (Tier 4 Utility)
|
|
if (this.path2_tier >= 4) {
|
|
if (typeof this._scav_timer === 'undefined') this._scav_timer = TD.exp_fps * 10; // Every 10 seconds
|
|
this._scav_timer--;
|
|
if (this._scav_timer <= 0) {
|
|
this._scav_timer = TD.exp_fps * (8 + Math.random() * 4); // Random 8-12 seconds
|
|
new TD.Loot("scav-" + TD.lang.rndStr(), { cx: this.cx, cy: this.cy, map: this.map, monster_bounty: 25 });
|
|
new TD.FloatingText("ft-" + TD.lang.rndStr(), { cx: this.cx, cy: this.cy - 20, text: "Found something!", color: "#ffcc00", map: this.map });
|
|
}
|
|
}
|
|
|
|
// 3. COMBAT PROTOCOLS (Path 1)
|
|
var target_monster = null;
|
|
var closestDistM = 999999;
|
|
var is_bumping = false;
|
|
|
|
// Scan for enemies
|
|
TD.lang.each(this.map.monsters, function(m) {
|
|
if (m.is_valid) {
|
|
var dist = Math.sqrt(Math.pow(m.cx - _this.cx, 2) + Math.pow(m.cy - _this.cy, 2));
|
|
|
|
// Track closest for Aggressive tracking
|
|
if (dist < closestDistM) { closestDistM = dist; target_monster = m; }
|
|
|
|
// Physical collision damage (Enemy hits Handy)
|
|
if (dist < m.r + _this.r) {
|
|
is_bumping = true;
|
|
_this.life -= m.damage;
|
|
if (TD.iframe % 5 === 0) {
|
|
new TD.FloatingText("hdmg-" + TD.lang.rndStr(), { cx: _this.cx, cy: _this.cy - 10, text: "-" + m.damage, color: "#ff0000", map: _this.map, size: 10, life: 16 });
|
|
}
|
|
|
|
// Tier 1 Combat: Circular Saw
|
|
if (_this.path1_tier >= 1 && TD.iframe % 3 === 0) {
|
|
var sawDmg = _this.path1_tier >= 4 ? 25 : 5; // Much stronger if Military Grade
|
|
m.beHit({ killed: 0, gainXp: function(){} }, sawDmg);
|
|
TD.Explode("saw-" + TD.lang.rndStr(), { cx: m.cx, cy: m.cy, r: 8, step_level: 1, render_level: 9, color: "#ff5500", scene: _this.map.scene, time: 0.1 });
|
|
}
|
|
}
|
|
|
|
// Tier 2 Combat: Laser Emitter
|
|
if (_this.path1_tier >= 2 && dist < TD.grid_size * 3) {
|
|
if (typeof _this._laser_timer === 'undefined') _this._laser_timer = 0;
|
|
_this._laser_timer--;
|
|
var fireRate = _this.path1_tier >= 4 ? 6 : 15; // Faster if Military Grade
|
|
if (_this._laser_timer <= 0) {
|
|
_this._laser_timer = fireRate;
|
|
var laserDmg = _this.path1_tier >= 4 ? 50 : 15;
|
|
m.beHit({ killed: 0, gainXp: function(){} }, laserDmg);
|
|
|
|
// NEW: Laser Sound
|
|
TD.playSound("snd_laser_fire");
|
|
|
|
// Draw the laser beam
|
|
var ctx = TD.ctx;
|
|
ctx.lineWidth = 2 * _TD.retina; ctx.strokeStyle = "rgba(255, 0, 0, 0.8)";
|
|
ctx.beginPath(); ctx.moveTo(_this.cx, _this.cy); ctx.lineTo(m.cx, m.cy); ctx.stroke();
|
|
}
|
|
}
|
|
|
|
// Tier 3 Combat: Flamer
|
|
if (_this.path1_tier >= 3 && dist < TD.grid_size * 1.5) {
|
|
if (TD.iframe % 5 === 0) {
|
|
m.is_poisoned = true;
|
|
m.poison_timer = TD.exp_fps * 2;
|
|
m.poison_damage = _this.path1_tier >= 4 ? 10 : 3;
|
|
TD.Explode("flame-" + TD.lang.rndStr(), { cx: m.cx, cy: m.cy, r: 12, step_level: 1, render_level: 9, color: "#ff8800", scene: _this.map.scene, time: 0.2 });
|
|
|
|
// NEW: Flamer Sound (reusing the shotgun blast for a heavy spray noise)
|
|
if (TD.iframe % 15 === 0) TD.playSound("snd_shotgun");
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (this.life <= 0) {
|
|
TD.Explode("handy-explode", { cx: this.cx, cy: this.cy, r: this.r*2, step_level: 1, render_level: 9, color: "#ffffaa", scene: this.map.scene, time: 0.5 });
|
|
this.is_valid = false;
|
|
this.map.mr_handy_active = false;
|
|
return;
|
|
}
|
|
|
|
// 4. BEHAVIOR & MOVEMENT LOGIC
|
|
this.target_loot = null;
|
|
var closestDistL = 999999;
|
|
if (this.map.loots && this.map.loots.length > 0) {
|
|
for (var i = 0; i < this.map.loots.length; i++) {
|
|
var l = this.map.loots[i];
|
|
if (l.is_valid && !l.is_collecting && l.fling_timer <= 0) {
|
|
var d = Math.sqrt(Math.pow(l.cx - this.cx, 2) + Math.pow(l.cy - this.cy, 2));
|
|
if (d < closestDistL) { closestDistL = d; this.target_loot = l; }
|
|
}
|
|
}
|
|
}
|
|
|
|
var tcx = this.cx, tcy = this.cy;
|
|
var evade_multiplier = 0; // Default: No evasion
|
|
|
|
// BEHAVIOR ROUTING
|
|
if (this.behavior === "aggressive" && target_monster) {
|
|
// Aggressive: Chase the closest monster. Ignore loot unless it's extremely close.
|
|
if (this.target_loot && closestDistL <= TD.grid_size * 2.5) {
|
|
tcx = this.target_loot.cx; tcy = this.target_loot.cy;
|
|
current_speed = this.loot_speed * TD.global_speed;
|
|
if (closestDistL <= this.r) this.target_loot.onClick();
|
|
} else {
|
|
tcx = target_monster.cx; tcy = target_monster.cy;
|
|
}
|
|
} else if (this.behavior === "defensive") {
|
|
// Defensive: Highly evasive. Gets loot, but runs away very fast if a monster gets close.
|
|
evade_multiplier = 2.5; // Huge push away from monsters
|
|
if (this.target_loot) {
|
|
tcx = this.target_loot.cx; tcy = this.target_loot.cy;
|
|
current_speed = this.loot_speed * TD.global_speed;
|
|
if (closestDistL <= this.r) this.target_loot.onClick();
|
|
} else {
|
|
// Safe to wander
|
|
if (this.wander_timer <= 0 || !this.wander_target) {
|
|
this.wander_target = { cx: this.map.x + (Math.random() * this.map.width), cy: this.map.y + (Math.random() * this.map.height) };
|
|
this.wander_timer = TD.exp_fps * 3;
|
|
} else {
|
|
tcx = this.wander_target.cx; tcy = this.wander_target.cy;
|
|
this.wander_timer--;
|
|
}
|
|
}
|
|
} else {
|
|
// Normal: Balances loot and evasion.
|
|
evade_multiplier = 1.0; // Moderate push away from monsters
|
|
if (this.target_loot) {
|
|
tcx = this.target_loot.cx; tcy = this.target_loot.cy;
|
|
current_speed = this.loot_speed * TD.global_speed;
|
|
if (closestDistL <= this.r) this.target_loot.onClick();
|
|
} else {
|
|
if (this.wander_timer <= 0 || !this.wander_target) {
|
|
this.wander_target = { cx: this.map.x + (Math.random() * this.map.width), cy: this.map.y + (Math.random() * this.map.height) };
|
|
this.wander_timer = TD.exp_fps * 3;
|
|
} else {
|
|
tcx = this.wander_target.cx; tcy = this.wander_target.cy;
|
|
this.wander_timer--;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Vector Flight & Obstacle Evasion ---
|
|
var dx = tcx - this.cx, dy = tcy - this.cy;
|
|
var c = Math.sqrt(dx*dx + dy*dy);
|
|
var move_x = 0, move_y = 0;
|
|
|
|
if (c > 0) {
|
|
move_x = (dx / c) * current_speed;
|
|
move_y = (dy / c) * current_speed;
|
|
}
|
|
|
|
// Apply Monster Avoidance if active
|
|
if (evade_multiplier > 0 && this.map.monsters) {
|
|
var evade_x = 0, evade_y = 0;
|
|
var avoid_radius = TD.grid_size * 1.5;
|
|
|
|
TD.lang.each(this.map.monsters, function(m) {
|
|
if (m.is_valid && m.life > 0) {
|
|
var mdx = _this.cx - m.cx;
|
|
var mdy = _this.cy - m.cy;
|
|
var dist = Math.sqrt(mdx*mdx + mdy*mdy);
|
|
|
|
if (dist > 0 && dist < avoid_radius) {
|
|
var force = Math.pow((avoid_radius - dist) / avoid_radius, 2);
|
|
evade_x += (mdx / dist) * force * current_speed * evade_multiplier;
|
|
evade_y += (mdy / dist) * force * current_speed * evade_multiplier;
|
|
}
|
|
}
|
|
});
|
|
move_x += evade_x; move_y += evade_y;
|
|
}
|
|
|
|
// Normalize final movement vector
|
|
var final_c = Math.sqrt(move_x*move_x + move_y*move_y);
|
|
if (final_c > 0 && final_c > current_speed) {
|
|
move_x = (move_x / final_c) * current_speed;
|
|
move_y = (move_y / final_c) * current_speed;
|
|
}
|
|
|
|
// Apply movement (snap perfectly to target if looting to prevent jitter)
|
|
if (c <= current_speed && this.target_loot && evade_multiplier === 0) {
|
|
this.cx = tcx; this.cy = tcy;
|
|
} else {
|
|
this.cx += move_x;
|
|
this.cy += move_y;
|
|
|
|
// Keep him on screen bounds
|
|
if (this.cx < this.map.x) this.cx = this.map.x;
|
|
if (this.cx > this.map.x2) this.cx = this.map.x2;
|
|
if (this.cy < this.map.y) this.cy = this.map.y;
|
|
if (this.cy > this.map.y2) this.cy = this.map.y2;
|
|
}
|
|
|
|
// Oscillating hover effect
|
|
this.hover_y = Math.sin(TD.iframe / 6) * 4 * _TD.retina;
|
|
this.caculatePos();
|
|
};
|
|
|
|
|
|
handy.render = function() {
|
|
if (!this.is_valid) return;
|
|
var ctx = TD.ctx;
|
|
|
|
// Draw the floating drop shadow on the ground
|
|
ctx.save();
|
|
ctx.translate(this.cx, this.cy + this.r); // Anchor shadow at his base
|
|
ctx.scale(1, 0.4); // Flatten into a perspective ellipse
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, this.r * 0.7, 0, Math.PI * 2, true);
|
|
ctx.fillStyle = "rgba(0,0,0,0.6)";
|
|
ctx.fill();
|
|
ctx.restore();
|
|
|
|
// Draw Image
|
|
if (this.img && this.img.complete) {
|
|
ctx.drawImage(this.img, this.x, this.y + this.hover_y, this.width, this.height);
|
|
} else {
|
|
// Fallback circle if image fails
|
|
ctx.fillStyle = "#aaa"; ctx.beginPath(); ctx.arc(this.cx, this.cy + this.hover_y, this.r, 0, Math.PI*2); ctx.fill();
|
|
}
|
|
|
|
// Health Bar
|
|
|
|
var hbw = 20 * _TD.retina, hbh = 3 * _TD.retina;
|
|
var hbx = this.cx - (hbw/2);
|
|
var hby = this.y + this.hover_y - (6 * _TD.retina);
|
|
|
|
ctx.fillStyle = "#000"; ctx.fillRect(hbx, hby, hbw, hbh);
|
|
ctx.fillStyle = "#0f0"; ctx.fillRect(hbx, hby, hbw * (this.life/this.max_life), hbh);
|
|
};
|
|
|
|
// --- NEW: MR HANDY HOVER & CLICK LOGIC ---
|
|
handy.onEnter = function () {
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (tooltip) {
|
|
var p1Name = this.path1_tier > 0 ? this.upgrades.path1[this.path1_tier - 1].name : "None";
|
|
var p2Name = this.path2_tier > 0 ? this.upgrades.path2[this.path2_tier - 1].name : "None";
|
|
|
|
var stats = "UNIT : " + this.name.toUpperCase() + "\n" +
|
|
"HP : " + Math.floor(this.life) + " / " + this.max_life + "\n" +
|
|
"MODE : " + this.behavior.toUpperCase() + "\n" +
|
|
"PATH 1: " + p1Name + "\n" +
|
|
"PATH 2: " + p2Name;
|
|
tooltip.innerText = stats;
|
|
tooltip.style.display = "block";
|
|
}
|
|
};
|
|
|
|
handy.onOut = function () {
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (tooltip) tooltip.style.display = "none";
|
|
};
|
|
|
|
handy.onClick = function () {
|
|
if (TD.is_paused || TD.admin_mode) return;
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (tooltip) tooltip.style.display = "none";
|
|
|
|
var overlay = document.getElementById("tower-modal-overlay");
|
|
if (!overlay) return;
|
|
|
|
document.getElementById("modal-tower-name").innerText = this.name.toUpperCase();
|
|
|
|
// NEW: Inject Mr. Handy's Image into the modal
|
|
var imgContainer = document.getElementById("modal-tower-img");
|
|
if (imgContainer) {
|
|
imgContainer.innerHTML = "<img src='assets/images/mrhandy.png' style='width: 100%; height: 100%; object-fit: contain; filter: drop-shadow(0 0 5px #0f0);'>";
|
|
}
|
|
|
|
var p1Name = this.path1_tier > 0 ? this.upgrades.path1[this.path1_tier - 1].name : "None";
|
|
var p2Name = this.path2_tier > 0 ? this.upgrades.path2[this.path2_tier - 1].name : "None";
|
|
|
|
|
|
var statsHtml = "<b>Level:</b> " + this.level + "<br>" +
|
|
"<b>HP:</b> " + Math.floor(this.life) + " / " + this.max_life + "<br><br>" +
|
|
"<b>Behavior:</b> " + this.behavior.toUpperCase() + "<br><br>" +
|
|
"<b style='color:#ffcc00;'>Path 1:</b> " + p1Name + "<br>" +
|
|
"<b style='color:#ffcc00;'>Path 2:</b> " + p2Name;
|
|
document.getElementById("modal-tower-stats").innerHTML = statsHtml;
|
|
|
|
var _handy = this;
|
|
var btnClose = document.getElementById("modal-close-btn");
|
|
var btnSell = document.getElementById("modal-btn-sell");
|
|
|
|
var newClose = btnClose.cloneNode(true); btnClose.parentNode.replaceChild(newClose, btnClose);
|
|
var newSell = btnSell.cloneNode(true); btnSell.parentNode.replaceChild(newSell, btnSell);
|
|
|
|
newClose.onclick = function() { overlay.style.display = "none"; };
|
|
|
|
newSell.innerText = "SCRAP UNIT (250c)";
|
|
newSell.onclick = function() {
|
|
TD.money += 250;
|
|
_handy.is_valid = false;
|
|
_handy.map.mr_handy_active = false;
|
|
TD.Explode("scrap-" + TD.lang.rndStr(), { cx: _handy.cx, cy: _handy.cy, r: 20, step_level: 1, render_level: 9, color: "#ffffaa", scene: _handy.map.scene, time: 0.3 });
|
|
overlay.style.display = "none";
|
|
};
|
|
|
|
overlay.onclick = function(e) { if(e.target === overlay) overlay.style.display = "none"; };
|
|
|
|
// Override the Targeting Dropdown for Behaviors
|
|
var selTarget = document.getElementById("modal-target-mode");
|
|
if (selTarget) {
|
|
selTarget.innerHTML = '<option value="normal">Normal (Balanced)</option><option value="defensive">Defensive (Evade)</option>';
|
|
// Only show Aggressive if he has a weapon
|
|
if (_handy.is_weapon) selTarget.innerHTML += '<option value="aggressive">Aggressive (Combat)</option>';
|
|
|
|
selTarget.value = _handy.behavior;
|
|
selTarget.onchange = function() { _handy.behavior = this.value; };
|
|
}
|
|
|
|
// Upgrades UI Logic
|
|
var drawIndicators = function(containerId, currentTier) {
|
|
var container = document.getElementById(containerId);
|
|
container.innerHTML = "";
|
|
for (var i = 0; i < 4; i++) {
|
|
var box = document.createElement("div");
|
|
box.style.width = "12px"; box.style.height = "12px"; box.style.border = "1px solid #0f0";
|
|
box.style.background = (i < currentTier) ? "#0f0" : "transparent";
|
|
container.appendChild(box);
|
|
}
|
|
};
|
|
|
|
var updateUpgradeUI = function(pathNum, btnElement, currentTier, otherTier, pathData) {
|
|
var nameEl = btnElement.querySelector("#path" + pathNum + "-name");
|
|
var costEl = btnElement.querySelector("#path" + pathNum + "-cost");
|
|
var descEl = btnElement.querySelector("#path" + pathNum + "-desc");
|
|
drawIndicators("path" + pathNum + "-indicators", currentTier);
|
|
|
|
var isRestricted = (currentTier === 2 && otherTier > 2);
|
|
if (currentTier >= 4) {
|
|
nameEl.innerText = "MAX TIER REACHED"; costEl.innerText = ""; descEl.innerText = "";
|
|
btnElement.disabled = true; btnElement.style.backgroundColor = "#444"; btnElement.style.borderColor = "#555";
|
|
nameEl.style.color = "#888"; descEl.style.color = "#888";
|
|
btnElement.removeAttribute("data-cost");
|
|
} else if (isRestricted) {
|
|
nameEl.innerText = "PATH LOCKED"; costEl.innerText = ""; descEl.innerText = "Cannot upgrade past Tier 2 because the other path is Tier 3+.";
|
|
btnElement.disabled = true; btnElement.style.backgroundColor = "#400"; btnElement.style.borderColor = "#800";
|
|
nameEl.style.color = "#888"; descEl.style.color = "#888";
|
|
btnElement.removeAttribute("data-cost");
|
|
} else {
|
|
var nextUpgrade = pathData[currentTier];
|
|
nameEl.innerText = nextUpgrade.name; costEl.innerText = nextUpgrade.cost + "c"; descEl.innerText = nextUpgrade.desc;
|
|
|
|
var actualCost = nextUpgrade.cost;
|
|
if (TD.perks && TD.perks.C) actualCost = Math.floor(actualCost * 0.85);
|
|
if (TD.perks && TD.perks.C) costEl.innerText = actualCost + "c";
|
|
|
|
btnElement.setAttribute("data-cost", actualCost);
|
|
|
|
if (TD.money >= actualCost) {
|
|
btnElement.disabled = false; btnElement.style.backgroundColor = "#2a7a2a"; btnElement.style.borderColor = "#0f0";
|
|
nameEl.style.color = "#000"; descEl.style.color = "#111";
|
|
} else {
|
|
btnElement.disabled = true; btnElement.style.backgroundColor = "#444"; btnElement.style.borderColor = "#555";
|
|
nameEl.style.color = "#888"; descEl.style.color = "#888";
|
|
}
|
|
|
|
// ALWAYS assign the click handler!
|
|
btnElement.onclick = function() {
|
|
if (TD.money >= actualCost) {
|
|
TD.money -= actualCost;
|
|
if (pathNum === 1) _handy.path1_tier++; else _handy.path2_tier++;
|
|
|
|
// Immediately apply logic flags based on tier
|
|
if (pathNum === 1 && _handy.path1_tier === 1) _handy.is_weapon = true; // Unlocks Aggressive
|
|
if (pathNum === 2 && _handy.path2_tier === 1) { _handy.max_life *= 3; _handy.life = _handy.max_life; } // HP First
|
|
if (pathNum === 2 && _handy.path2_tier === 2) _handy.base_speed *= 1.5; // Speed Second
|
|
if (pathNum === 2 && _handy.path2_tier === 3) _handy.magnetic_coil = true;
|
|
|
|
TD.Explode("upg-" + TD.lang.rndStr(), { cx: _handy.cx, cy: _handy.cy, r: 20 * _TD.retina, step_level: 1, render_level: 9, color: "#00ff00", scene: _handy.map.scene, time: 0.3 });
|
|
_handy.onClick(); // Re-render UI
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
var btnPath1 = document.getElementById("modal-btn-path1");
|
|
var btnPath2 = document.getElementById("modal-btn-path2");
|
|
var newP1 = btnPath1.cloneNode(true); btnPath1.parentNode.replaceChild(newP1, btnPath1);
|
|
var newP2 = btnPath2.cloneNode(true); btnPath2.parentNode.replaceChild(newP2, btnPath2);
|
|
|
|
updateUpgradeUI(1, newP1, _handy.path1_tier, _handy.path2_tier, _handy.upgrades.path1);
|
|
updateUpgradeUI(2, newP2, _handy.path2_tier, _handy.path1_tier, _handy.upgrades.path2);
|
|
|
|
overlay.style.display = "flex";
|
|
};
|
|
|
|
|
|
handy._init(cfg);
|
|
return handy;
|
|
};
|
|
|
|
|
|
|
|
// NEW: Cryo Mine Engine Object
|
|
|
|
TD.CryoMine = function (id, cfg) {
|
|
cfg.on_events = ["click", "enter", "out"];
|
|
var mine = new TD.Element(id, cfg);
|
|
|
|
mine._init = function(cfg) {
|
|
this.grid = cfg.grid;
|
|
this.map = cfg.map;
|
|
this.cx = this.grid.cx;
|
|
this.cy = this.grid.cy;
|
|
|
|
var r = TD.grid_size * 0.3;
|
|
this.x = this.cx - r; this.y = this.cy - r;
|
|
this.width = r * 2; this.height = r * 2;
|
|
this.x2 = this.x + this.width; this.y2 = this.y + this.height;
|
|
|
|
this.is_valid = true;
|
|
this.pulse = 0;
|
|
|
|
this.blast_radius = TD.grid_size * 3.5;
|
|
|
|
this.addToScene(this.map.scene, 1, 4);
|
|
};
|
|
|
|
mine.onEnter = function() { this.map.scene.panel.balloontip.msg("CRYO MINE", this); };
|
|
mine.onOut = function() { this.map.scene.panel.balloontip.hide(); };
|
|
|
|
mine.detonate = function() {
|
|
if (!this.is_valid) return;
|
|
this.is_valid = false;
|
|
|
|
var ctx = TD.ctx;
|
|
var cx = this.cx;
|
|
var cy = this.cy;
|
|
var r2 = Math.pow(this.blast_radius, 2);
|
|
|
|
TD.Explode("explode-" + TD.lang.rndStr(), {
|
|
cx: cx, cy: cy, r: this.blast_radius / _TD.retina,
|
|
step_level: this.step_level, render_level: 9,
|
|
color: "#00ffff", scene: this.map.scene, time: 0.5
|
|
});
|
|
|
|
TD.lang.each(this.map.monsters, function (m) {
|
|
if (m.is_valid) {
|
|
var dist2 = Math.pow(m.cx - cx, 2) + Math.pow(m.cy - cy, 2);
|
|
if (dist2 <= r2) {
|
|
if (!m.is_frozen) {
|
|
m.is_frozen = true;
|
|
m.original_speed = m.speed;
|
|
}
|
|
m.speed = 0.1; // Almost 0 to stop movement without breaking math
|
|
m.freeze_timer = 10 * 24; // Doubled to 10 seconds (240 frames)
|
|
}
|
|
}
|
|
});
|
|
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "CRYO MINE DETONATED!";
|
|
};
|
|
|
|
mine.step = function () {
|
|
if (!this.is_valid) return;
|
|
this.pulse += 0.1;
|
|
|
|
var cx = this.cx, cy = this.cy;
|
|
var trigger_dist = Math.pow(TD.grid_size * 1.0, 2);
|
|
var triggered = TD.lang.any(this.map.monsters, function (m) {
|
|
return (Math.pow(m.cx - cx, 2) + Math.pow(m.cy - cy, 2) <= trigger_dist);
|
|
});
|
|
|
|
if (triggered) this.detonate();
|
|
};
|
|
|
|
|
|
mine.render = function () {
|
|
if (!this.is_valid) return;
|
|
var ctx = TD.ctx;
|
|
|
|
var glow = Math.abs(Math.sin(this.pulse));
|
|
|
|
ctx.shadowBlur = 10 * glow;
|
|
ctx.shadowColor = "#00ffff";
|
|
ctx.fillStyle = "#aaffff";
|
|
ctx.beginPath();
|
|
ctx.arc(this.cx, this.cy, 4 * _TD.retina, 0, Math.PI * 2, true);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
|
|
ctx.strokeStyle = "#555";
|
|
ctx.lineWidth = 2 * _TD.retina;
|
|
ctx.beginPath();
|
|
ctx.arc(this.cx, this.cy, 6 * _TD.retina, 0, Math.PI * 2, true);
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
};
|
|
|
|
mine.onClick = function () {
|
|
if (!this.is_valid) return;
|
|
this.detonate();
|
|
};
|
|
|
|
mine._init(cfg);
|
|
return mine;
|
|
};
|
|
|
|
// NEW: Cap Mine Engine Object
|
|
TD.CapMine = function (id, cfg) {
|
|
cfg.on_events = ["click", "enter", "out"];
|
|
var mine = new TD.Element(id, cfg);
|
|
|
|
mine._init = function(cfg) {
|
|
this.grid = cfg.grid; this.map = cfg.map;
|
|
this.cx = this.grid.cx; this.cy = this.grid.cy;
|
|
var r = TD.grid_size * 0.3;
|
|
this.x = this.cx - r; this.y = this.cy - r;
|
|
this.width = r * 2; this.height = r * 2;
|
|
this.x2 = this.x + this.width; this.y2 = this.y + this.height;
|
|
|
|
this.is_valid = true; this.pulse = 0;
|
|
this.blast_radius = TD.grid_size * 3.0;
|
|
this.addToScene(this.map.scene, 1, 4);
|
|
};
|
|
|
|
mine.onEnter = function() { this.map.scene.panel.balloontip.msg("CAP MINE", this); };
|
|
mine.onOut = function() { this.map.scene.panel.balloontip.hide(); };
|
|
|
|
mine.detonate = function() {
|
|
if (!this.is_valid) return;
|
|
this.is_valid = false;
|
|
var cx = this.cx, cy = this.cy, r2 = Math.pow(this.blast_radius, 2);
|
|
|
|
TD.Explode("explode-" + TD.lang.rndStr(), { cx: cx, cy: cy, r: this.blast_radius / _TD.retina, step_level: this.step_level, render_level: 9, color: "#ffaa00", scene: this.map.scene, time: 0.5 });
|
|
|
|
var dummy_source = { killed: 0, gainXp: function(){} };
|
|
TD.lang.each(this.map.monsters, function (m) {
|
|
if (m.is_valid && (Math.pow(m.cx - cx, 2) + Math.pow(m.cy - cy, 2) <= r2)) {
|
|
m.beHit(dummy_source, 500);
|
|
}
|
|
});
|
|
|
|
// Spit out 3-5 coins worth 5-15 each
|
|
var coin_count = Math.floor(Math.random() * 3) + 3;
|
|
for(var i=0; i<coin_count; i++) {
|
|
var coin_val = Math.floor(Math.random() * 11) + 5;
|
|
var loot = new TD.Loot("loot-" + TD.lang.rndStr(), { cx: cx, cy: cy, map: this.map, monster_bounty: coin_val });
|
|
loot.loot_type = "money"; // Force it to be money
|
|
loot.value = coin_val;
|
|
}
|
|
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "CAP MINE DETONATED!";
|
|
};
|
|
|
|
mine.step = function () {
|
|
if (!this.is_valid) return;
|
|
this.pulse += 0.1;
|
|
var cx = this.cx, cy = this.cy, trigger_dist = Math.pow(TD.grid_size * 1.0, 2);
|
|
if (TD.lang.any(this.map.monsters, function (m) { return (Math.pow(m.cx - cx, 2) + Math.pow(m.cy - cy, 2) <= trigger_dist); })) this.detonate();
|
|
};
|
|
|
|
|
|
mine.render = function () {
|
|
if (!this.is_valid) return;
|
|
var ctx = TD.ctx, glow = Math.abs(Math.sin(this.pulse));
|
|
ctx.shadowBlur = 10 * glow; ctx.shadowColor = "#ffaa00";
|
|
ctx.fillStyle = "#ffcc00";
|
|
ctx.beginPath(); ctx.arc(this.cx, this.cy, 4 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.shadowBlur = 0;
|
|
ctx.strokeStyle = "#555"; ctx.lineWidth = 2 * _TD.retina;
|
|
ctx.beginPath(); ctx.arc(this.cx, this.cy, 6 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.stroke();
|
|
};
|
|
|
|
mine.onClick = function () { if (this.is_valid) this.detonate(); };
|
|
mine._init(cfg); return mine;
|
|
};
|
|
|
|
|
|
// NEW: Floating Text Engine Object
|
|
TD.FloatingText = function (id, cfg) {
|
|
var ft = new TD.Element(id, cfg);
|
|
ft._init = function(cfg) {
|
|
this.cx = cfg.cx; this.cy = cfg.cy; this.text = cfg.text;
|
|
this.color = cfg.color || "#fff"; this.map = cfg.map;
|
|
this.size = cfg.size || 16;
|
|
this.is_valid = true; this.life = cfg.life || 36; this.max_life = this.life;
|
|
this.addToScene(this.map.scene, 1, 9);
|
|
};
|
|
|
|
ft.step = function () {
|
|
if (!this.is_valid) return;
|
|
this.cy -= 1.0; this.life--;
|
|
if (this.life <= 0) this.is_valid = false;
|
|
};
|
|
ft.render = function () {
|
|
if (!this.is_valid) return;
|
|
var ctx = TD.ctx;
|
|
ctx.globalAlpha = Math.sin((this.life / this.max_life) * Math.PI);
|
|
ctx.fillStyle = this.color; ctx.font = "bold " + Math.floor(this.size * _TD.retina) + "px Arial";
|
|
|
|
ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
ctx.shadowBlur = 4; ctx.shadowColor = "#000";
|
|
ctx.fillText(this.text, this.cx, this.cy);
|
|
ctx.globalAlpha = 1.0; ctx.shadowBlur = 0;
|
|
};
|
|
ft._init(cfg);
|
|
return ft;
|
|
};
|
|
|
|
// --- NEW: ULTIMATE ABILITY ELEMENTS ---
|
|
|
|
// 1. BOS Orbital Laser (Mouse Tracking)
|
|
TD.OrbitalLaser = function (id, cfg) {
|
|
var el = new TD.Element(id, cfg);
|
|
el._init = function(cfg) {
|
|
this.map = cfg.map; this.cx = cfg.cx; this.cy = cfg.cy;
|
|
this.life = 8 * 24; this.r = TD.grid_size * 1.5;
|
|
this.is_valid = true; this.addToScene(this.map.scene, 1, 9);
|
|
var _this = this;
|
|
this._mm = function(e) {
|
|
var rect = TD.canvas.getBoundingClientRect();
|
|
_this.cx = (e.clientX - rect.left) * (TD.canvas.width / rect.width);
|
|
_this.cy = (e.clientY - rect.top) * (TD.canvas.height / rect.height);
|
|
};
|
|
window.addEventListener("mousemove", this._mm);
|
|
};
|
|
el.step = function() {
|
|
if (!this.is_valid) return;
|
|
this.life--;
|
|
if (this.life <= 0) { this.is_valid = false; window.removeEventListener("mousemove", this._mm); return; }
|
|
var _this = this; var dummy = { killed: 0, gainXp: function(){} };
|
|
TD.lang.each(this.map.monsters, function(m) {
|
|
if (m.is_valid && Math.pow(m.cx - _this.cx, 2) + Math.pow(m.cy - _this.cy, 2) <= Math.pow(_this.r, 2)) m.beHit(dummy, 15); // 15 dmg per frame
|
|
});
|
|
if (Math.random() < 0.4) TD.Explode("orb-p-"+TD.lang.rndStr(), {cx: this.cx + (Math.random()*this.r - this.r/2), cy: this.cy + (Math.random()*this.r - this.r/2), r: 8, step_level:1, render_level:9, color: "#00aaff", scene: this.map.scene, time: 0.2});
|
|
};
|
|
el.render = function() {
|
|
if (!this.is_valid) return;
|
|
var ctx = TD.ctx; ctx.shadowBlur = 30; ctx.shadowColor = "#00aaff";
|
|
ctx.fillStyle = "rgba(100, 200, 255, 0.6)"; ctx.beginPath(); ctx.arc(this.cx, this.cy, this.r, 0, Math.PI*2); ctx.fill();
|
|
ctx.fillStyle = "#ffffff"; ctx.beginPath(); ctx.arc(this.cx, this.cy, this.r * 0.4, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0;
|
|
};
|
|
el._init(cfg); return el;
|
|
};
|
|
|
|
// 2. Minutemen Militia Blockade
|
|
TD.MilitiaBlockade = function(id, cfg) {
|
|
var el = new TD.Element(id, cfg);
|
|
el._init = function(cfg) {
|
|
this.map = cfg.map; this.cx = cfg.cx; this.cy = cfg.cy;
|
|
this.max_life = 1000; this.life = this.max_life;
|
|
this.r = TD.grid_size * 1.0; this.is_valid = true;
|
|
this.addToScene(this.map.scene, 1, 4);
|
|
};
|
|
el.step = function() {
|
|
if (!this.is_valid) return;
|
|
var _this = this; var dummy = { killed: 0, gainXp: function(){} }; var isAttacked = false;
|
|
TD.lang.each(this.map.monsters, function(m) {
|
|
if (m.is_valid && Math.pow(m.cx - _this.cx, 2) + Math.pow(m.cy - _this.cy, 2) <= Math.pow(_this.r * 1.5, 2)) {
|
|
m.speed = 0.1; m.is_frozen = true; m.freeze_timer = 5; // Halt monster
|
|
_this.life -= m.damage; isAttacked = true;
|
|
if (TD.iframe % 12 === 0) m.beHit(dummy, 30); // Shoot back
|
|
}
|
|
});
|
|
if (isAttacked) TD.Explode("m-hit-"+TD.lang.rndStr(), {cx: this.cx, cy: this.cy, r: 10, step_level:1, render_level:9, color:"#ff0000", scene:this.map.scene, time:0.1});
|
|
if (this.life <= 0) this.is_valid = false;
|
|
};
|
|
el.render = function() {
|
|
if (!this.is_valid) return;
|
|
var ctx = TD.ctx; ctx.fillStyle = "#4a6887";
|
|
ctx.beginPath(); ctx.arc(this.cx - 10, this.cy + 5, 8, 0, Math.PI*2); ctx.fill();
|
|
ctx.beginPath(); ctx.arc(this.cx + 10, this.cy + 5, 8, 0, Math.PI*2); ctx.fill();
|
|
ctx.beginPath(); ctx.arc(this.cx, this.cy - 12, 8, 0, Math.PI*2); ctx.fill();
|
|
ctx.fillStyle = "#000"; ctx.fillRect(this.cx - 15, this.cy - 25, 30, 4);
|
|
ctx.fillStyle = "#0f0"; ctx.fillRect(this.cx - 15, this.cy - 25, 30 * (this.life/this.max_life), 4);
|
|
};
|
|
el._init(cfg); return el;
|
|
};
|
|
|
|
// 3. Raider Nuka-Grenade (Radiation Zone)
|
|
TD.RadZone = function(id, cfg) {
|
|
var el = new TD.Element(id, cfg);
|
|
el._init = function(cfg) {
|
|
this.map = cfg.map; this.cx = cfg.cx; this.cy = cfg.cy;
|
|
this.life = 10 * 24; this.r = TD.grid_size * 3.5; this.is_valid = true;
|
|
this.addToScene(this.map.scene, 1, 3);
|
|
TD.Explode("nuka-blast", {cx: this.cx, cy: this.cy, r: this.r, step_level:1, render_level:9, color:"#ffcc00", scene:this.map.scene, time:1.0});
|
|
var dummy = { killed: 0, gainXp: function(){} }; var _this = this;
|
|
TD.lang.each(this.map.monsters, function(m) {
|
|
if (m.is_valid && Math.pow(m.cx - _this.cx, 2) + Math.pow(m.cy - _this.cy, 2) <= Math.pow(_this.r, 2)) m.beHit(dummy, 600);
|
|
});
|
|
};
|
|
el.step = function() {
|
|
if (!this.is_valid) return;
|
|
this.life--; if (this.life <= 0) { this.is_valid = false; return; }
|
|
var dummy = { killed: 0, gainXp: function(){} }; var _this = this;
|
|
TD.lang.each(this.map.monsters, function(m) {
|
|
if (m.is_valid && Math.pow(m.cx - _this.cx, 2) + Math.pow(m.cy - _this.cy, 2) <= Math.pow(_this.r, 2)) m.beHit(dummy, 6); // DoT
|
|
});
|
|
};
|
|
el.render = function() {
|
|
if (!this.is_valid) return;
|
|
var ctx = TD.ctx; ctx.shadowBlur = 15; ctx.shadowColor = "#00ff00";
|
|
ctx.fillStyle = "rgba(50, 255, 50, 0.25)"; ctx.beginPath(); ctx.arc(this.cx, this.cy, this.r, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0;
|
|
};
|
|
el._init(cfg); return el;
|
|
};
|
|
|
|
// NEW: Hard Reset / Cancel Build Mode function
|
|
|
|
var forceCancelBuild = function(e) {
|
|
if (typeof TD === 'undefined' || !TD.stage) return;
|
|
|
|
// Only prevent right-click menu if we are actually cancelling something
|
|
var isDeploying = TD.mode && TD.mode.indexOf("deploy_") === 0;
|
|
if (e && e.type === "contextmenu" && (TD.mode === "build" || isDeploying || (TD.stage.current_act && TD.stage.current_act.current_scene.map.selected_building) || TD.current_editor_asset)) {
|
|
e.preventDefault();
|
|
}
|
|
|
|
try {
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
|
|
if (TD.mode === "build") map.cancelPreBuild();
|
|
|
|
if (isDeploying) {
|
|
TD.mode = "normal";
|
|
TD.mouseHand(false);
|
|
var d = document.getElementById("ui-intel-brief");
|
|
if (d) d.innerText = "DEPLOYMENT CANCELLED.";
|
|
}
|
|
|
|
if (map.selected_building) {
|
|
map.selected_building.is_selected = false;
|
|
|
|
map.selected_building = null;
|
|
}
|
|
if (map.select_hl) map.select_hl.hide();
|
|
if (map.scene && map.scene.panel && map.scene.panel.balloontip) {
|
|
map.scene.panel.balloontip.hide();
|
|
}
|
|
|
|
// NEW: Clear the asset brush safely
|
|
if (TD.current_editor_asset) {
|
|
TD.current_editor_asset = null;
|
|
TD.admin_brush = "clear";
|
|
var d = document.getElementById("ui-action-desc");
|
|
if (d) d.innerText = "Brush cleared.";
|
|
}
|
|
|
|
} catch(err) {}
|
|
};
|
|
|
|
|
|
// Listeners
|
|
document.addEventListener("keydown", function(e) {
|
|
if (e.key === "Escape") forceCancelBuild(e);
|
|
|
|
// --- NEW: DEV CHEAT WINDOW TOGGLE ---
|
|
if (e.key === "`" || e.key === "~") {
|
|
var w = document.getElementById("dev-cheat-window");
|
|
if (w) w.style.display = (w.style.display === "none") ? "block" : "none";
|
|
}
|
|
});
|
|
|
|
// Listeners
|
|
var btnVats = document.getElementById("btn-vats-activate");
|
|
if (btnVats) {
|
|
btnVats.onclick = function() {
|
|
if (typeof TD !== 'undefined') TD.startVats();
|
|
};
|
|
}
|
|
|
|
|
|
document.addEventListener("contextmenu", forceCancelBuild);
|
|
|
|
// --- NEW: DEV WINDOW LOGIC & DRAGGING ---
|
|
// Dragging Logic
|
|
var cheatWindow = document.getElementById("dev-cheat-window");
|
|
var cheatHeader = document.getElementById("dev-cheat-header");
|
|
if (cheatWindow && cheatHeader) {
|
|
var isDragging = false, offsetX, offsetY;
|
|
cheatHeader.addEventListener("mousedown", function(e) {
|
|
isDragging = true;
|
|
var rect = cheatWindow.getBoundingClientRect();
|
|
offsetX = e.clientX - rect.left;
|
|
offsetY = e.clientY - rect.top;
|
|
});
|
|
document.addEventListener("mousemove", function(e) {
|
|
if (!isDragging) return;
|
|
cheatWindow.style.left = (e.clientX - offsetX) + "px";
|
|
cheatWindow.style.top = (e.clientY - offsetY) + "px";
|
|
cheatWindow.style.right = "auto";
|
|
cheatWindow.style.bottom = "auto";
|
|
});
|
|
document.addEventListener("mouseup", function() { isDragging = false; });
|
|
}
|
|
|
|
// Button Logic
|
|
document.getElementById("dev-btn-caps").onclick = function() {
|
|
if (typeof TD === 'undefined') return;
|
|
var val = parseInt(document.getElementById("dev-input-caps").value) || 0;
|
|
TD.money += val;
|
|
};
|
|
document.getElementById("dev-btn-score").onclick = function() {
|
|
if (typeof TD === 'undefined') return;
|
|
var val = parseInt(document.getElementById("dev-input-score").value) || 0;
|
|
TD.score += val;
|
|
};
|
|
document.getElementById("dev-btn-health").onclick = function() {
|
|
if (typeof TD === 'undefined') return;
|
|
var val = parseInt(document.getElementById("dev-input-health").value) || 100;
|
|
if (TD.stage && TD.stage.current_act && TD.stage.current_act.current_scene.map.map_type === "siege") {
|
|
TD.base_health = val;
|
|
} else {
|
|
TD.life = val;
|
|
}
|
|
};
|
|
|
|
document.getElementById("dev-btn-vats").onclick = function() {
|
|
if (typeof TD === 'undefined' || typeof TD.vats_charge === 'undefined') return;
|
|
var val = parseInt(document.getElementById("dev-input-vats").value) || 0;
|
|
if (val < 0) val = 0;
|
|
if (val > 100) val = 100;
|
|
TD.vats_charge = (val / 100) * TD.vats_max_charge;
|
|
};
|
|
|
|
document.getElementById("dev-btn-wave").onclick = function() {
|
|
if (typeof TD === 'undefined' || !TD.stage || !TD.stage.current_act || !TD.stage.current_act.current_scene) return;
|
|
var val = parseInt(document.getElementById("dev-input-wave").value) || 1;
|
|
if (val < 1) val = 1;
|
|
|
|
var scene = TD.stage.current_act.current_scene;
|
|
var activeMap = scene.map;
|
|
|
|
// Wipe existing monsters so waves don't mix
|
|
if (activeMap) {
|
|
TD.lang.each(activeMap.monsters, function(m) { m.is_valid = false; });
|
|
activeMap.monsters = [];
|
|
activeMap._wait_add_monsters = 0;
|
|
activeMap._wait_add_monsters_arr = [];
|
|
}
|
|
|
|
// Set internal wave to 1 less than target, so it spawns perfectly on next wave start
|
|
scene.wave = val - 1;
|
|
|
|
// FIX: Clear the wave generation lock so the Intel script and spawner recognize the jump!
|
|
scene._wave_generated_for = -1;
|
|
scene._intel_chatter_generated = false;
|
|
|
|
TD.stage.wait_new_wave = TD.exp_fps * 5; // Reset 5 second spawn timer
|
|
|
|
// Push the update directly to the Intel text to overwrite whatever was there
|
|
if (typeof TD.logEvent === 'function') TD.logEvent("OVERSEER OVERRIDE: INITIATING JUMP TO WAVE " + val);
|
|
};
|
|
|
|
|
|
document.getElementById("dev-btn-spawn").onclick = function() {
|
|
if (typeof TD === 'undefined' || !TD.stage || !TD.stage.current_act || !TD.stage.current_act.current_scene) return;
|
|
var val = parseInt(document.getElementById("dev-sel-spawn").value) || 0;
|
|
var map = TD.stage.current_act.current_scene.map;
|
|
if (map) {
|
|
map.addMonster(val);
|
|
}
|
|
};
|
|
|
|
document.getElementById("dev-btn-tower").onclick = function() {
|
|
|
|
|
|
if (typeof TD === 'undefined' || !TD.factionData || !TD.current_faction) return;
|
|
var val = document.getElementById("dev-sel-tower").value;
|
|
var towers = TD.factionData[TD.current_faction];
|
|
if (val === "all") {
|
|
for (var i = 0; i < towers.length; i++) towers[i].is_unlocked = true;
|
|
} else {
|
|
var idx = parseInt(val.replace("t", "")) - 1;
|
|
if (towers[idx]) towers[idx].is_unlocked = true;
|
|
}
|
|
TD.loadFaction(TD.current_faction);
|
|
};
|
|
document.getElementById("dev-btn-item").onclick = function() {
|
|
if (typeof TD === 'undefined' || !TD.inventory) return;
|
|
var item = document.getElementById("dev-sel-item").value;
|
|
var val = parseInt(document.getElementById("dev-input-item").value) || 1;
|
|
TD.inventory[item] += val;
|
|
if (TD.inventory[item] > 10) TD.inventory[item] = 10;
|
|
TD.updateItemButton(item);
|
|
};
|
|
document.getElementById("dev-btn-drop").onclick = function() {
|
|
window.devForcedDrop = document.getElementById("dev-sel-drop").value;
|
|
var d = document.getElementById("ui-intel-brief");
|
|
if (d) d.innerText = "NEXT DROP FORCED: " + window.devForcedDrop.toUpperCase();
|
|
};
|
|
|
|
|
|
// --- NEW: ASSET MODAL LOGIC ---
|
|
|
|
TD.populateAssetModal = function() {
|
|
var themeSelect = document.getElementById("asset-theme-select");
|
|
var gridContainer = document.getElementById("asset-grid-container");
|
|
if (!themeSelect || !gridContainer) return;
|
|
|
|
// Populate Theme Dropdown if empty
|
|
if (themeSelect.options.length === 0) {
|
|
for (var theme in TD.assetLibrary) {
|
|
var opt = document.createElement("option");
|
|
opt.value = theme;
|
|
opt.innerHTML = theme;
|
|
themeSelect.appendChild(opt);
|
|
}
|
|
// Add listener to change grid when theme changes
|
|
themeSelect.onchange = function() { TD.populateAssetModal(); };
|
|
}
|
|
|
|
var currentTheme = themeSelect.value;
|
|
var assets = TD.assetLibrary[currentTheme] || [];
|
|
|
|
// Clear existing grid
|
|
gridContainer.innerHTML = "";
|
|
|
|
// Build thumbnail cards
|
|
TD.lang.each(assets, function(asset) {
|
|
var card = document.createElement("div");
|
|
card.style.background = "#222";
|
|
card.style.border = "1px solid #0f0";
|
|
card.style.padding = "5px";
|
|
card.style.cursor = "pointer";
|
|
card.style.textAlign = "center";
|
|
card.style.display = "flex";
|
|
card.style.flexDirection = "column";
|
|
card.style.justifyContent = "space-between";
|
|
|
|
// Hover effect
|
|
card.onmouseover = function() { card.style.background = "#050"; };
|
|
card.onmouseout = function() { card.style.background = "#222"; };
|
|
|
|
// Image Thumbnail
|
|
var img = document.createElement("img");
|
|
img.src = asset.src;
|
|
img.style.maxWidth = "100%";
|
|
img.style.maxHeight = "80px";
|
|
img.style.objectFit = "contain";
|
|
img.style.marginBottom = "5px";
|
|
// Error fallback if image doesn't exist
|
|
img.onerror = function() { this.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50"><rect width="50" height="50" fill="%23f00"/><text x="10" y="30" fill="white" font-family="sans-serif" font-size="20">?</text></svg>'; };
|
|
|
|
var label = document.createElement("div");
|
|
label.style.fontSize = "0.9em";
|
|
label.innerHTML = "<b>" + asset.name + "</b><br><span style='color:#aaa; font-size:0.8em;'>" + asset.grid_x + "x" + asset.grid_y + (asset.blocks_path ? " (Solid)" : "") + "</span>";
|
|
|
|
card.appendChild(img);
|
|
card.appendChild(label);
|
|
|
|
// Click to select
|
|
card.onclick = function() {
|
|
TD.current_editor_asset = asset;
|
|
TD.admin_brush = "asset"; // Set the engine brush mode to asset placing
|
|
|
|
// Close modal and set description
|
|
document.getElementById("asset-modal-overlay").style.display = "none";
|
|
var uiDesc = document.getElementById("ui-action-desc");
|
|
if (uiDesc) uiDesc.innerHTML = "ASSET BRUSH: Placing " + asset.name + " (" + asset.grid_x + "x" + asset.grid_y + "). Click map to drop.";
|
|
};
|
|
|
|
gridContainer.appendChild(card);
|
|
});
|
|
};
|
|
|
|
});
|
|
|
|
_TD.a.push(function (TD) {
|
|
// --- NEW: WAVE EDITOR LOGIC ---
|
|
TD.custom_wave_data = {};
|
|
for (var i = 1; i <= 15; i++) {
|
|
TD.custom_wave_data[i] = {
|
|
story_enable: false,
|
|
story_data: {
|
|
"ALL": { title: "", desc: "" },
|
|
"NCR": { title: "", desc: "" },
|
|
"BOS": { title: "", desc: "" },
|
|
"MINUTEMEN": { title: "", desc: "" },
|
|
"RAIDERS": { title: "", desc: "" }
|
|
},
|
|
towers: {}, // e.g. { "tower_1": "unlocked", "tower_2": "locked", "tower_3": "hidden" }
|
|
spawns: [] // e.g. [ { qty: 5, monster_idx: 0 } ]
|
|
};
|
|
}
|
|
TD.we_current_wave = 1;
|
|
TD.we_current_faction_tab = "ALL";
|
|
|
|
TD.we_refreshWaveList = function() {
|
|
var waveSelect = document.getElementById("wave-editor-select");
|
|
waveSelect.innerHTML = ""; // Clear existing options
|
|
|
|
var numWaves = Object.keys(TD.custom_wave_data).length;
|
|
if (numWaves === 0) {
|
|
// Failsafe
|
|
TD.custom_wave_data[1] = { story_enable: false, story_data: { "ALL": {title:"", desc:""}, "NCR": {title:"", desc:""}, "BOS": {title:"", desc:""}, "MINUTEMEN": {title:"", desc:""}, "RAIDERS": {title:"", desc:""} }, towers: {}, spawns: [] };
|
|
numWaves = 1;
|
|
}
|
|
|
|
for (var i = 1; i <= numWaves; i++) {
|
|
var opt = document.createElement("option");
|
|
opt.value = i;
|
|
opt.innerText = "WAVE " + i;
|
|
waveSelect.appendChild(opt);
|
|
}
|
|
|
|
if (TD.we_current_wave > numWaves) TD.we_current_wave = numWaves;
|
|
if (TD.we_current_wave < 1) TD.we_current_wave = 1;
|
|
waveSelect.value = TD.we_current_wave;
|
|
};
|
|
|
|
TD.populateWaveEditor = function() {
|
|
var waveSelect = document.getElementById("wave-editor-select");
|
|
|
|
if (!waveSelect.onchange) { // Only wire up events once
|
|
waveSelect.onchange = function() {
|
|
TD.we_saveCurrentWave();
|
|
TD.we_current_wave = parseInt(this.value);
|
|
TD.we_loadCurrentWave();
|
|
};
|
|
|
|
document.getElementById("we-btn-add-spawn").onclick = function() {
|
|
TD.custom_wave_data[TD.we_current_wave].spawns.push({ qty: 1, monster_idx: 0 });
|
|
TD.we_renderSpawns();
|
|
};
|
|
|
|
var facSelect = document.getElementById("we-story-faction");
|
|
if (facSelect) {
|
|
facSelect.onchange = function() {
|
|
TD.we_saveCurrentWave();
|
|
TD.we_current_faction_tab = this.value;
|
|
TD.we_loadCurrentWave();
|
|
};
|
|
}
|
|
|
|
// --- NEW: Add / Delete Wave Buttons ---
|
|
var btnAddWave = document.getElementById("we-btn-add-wave");
|
|
if (btnAddWave) {
|
|
btnAddWave.onclick = function() {
|
|
TD.we_saveCurrentWave();
|
|
var newWaveNum = Object.keys(TD.custom_wave_data).length + 1;
|
|
TD.custom_wave_data[newWaveNum] = {
|
|
story_enable: false,
|
|
story_data: { "ALL": {title:"", desc:""}, "NCR": {title:"", desc:""}, "BOS": {title:"", desc:""}, "MINUTEMEN": {title:"", desc:""}, "RAIDERS": {title:"", desc:""} },
|
|
towers: {}, spawns: []
|
|
};
|
|
TD.we_current_wave = newWaveNum;
|
|
TD.we_refreshWaveList();
|
|
TD.we_loadCurrentWave();
|
|
};
|
|
}
|
|
|
|
var btnDelWave = document.getElementById("we-btn-del-wave");
|
|
if (btnDelWave) {
|
|
btnDelWave.onclick = function() {
|
|
var numWaves = Object.keys(TD.custom_wave_data).length;
|
|
if (numWaves <= 1) {
|
|
alert("You cannot delete the last wave!");
|
|
return;
|
|
}
|
|
if (confirm("Are you sure you want to delete Wave " + TD.we_current_wave + "?")) {
|
|
// Shift all subsequent waves down by 1
|
|
for (var i = TD.we_current_wave; i < numWaves; i++) {
|
|
TD.custom_wave_data[i] = TD.custom_wave_data[i + 1];
|
|
}
|
|
// Delete the now-duplicate last element
|
|
delete TD.custom_wave_data[numWaves];
|
|
|
|
TD.we_refreshWaveList();
|
|
TD.we_loadCurrentWave();
|
|
}
|
|
};
|
|
}
|
|
|
|
// --- NEW: Copy Previous Wave Tower Settings ---
|
|
var btnCopyPrev = document.getElementById("we-btn-copy-prev");
|
|
if (btnCopyPrev) {
|
|
btnCopyPrev.onclick = function() {
|
|
if (TD.we_current_wave > 1) {
|
|
TD.we_saveCurrentWave(); // Save any story text first so we don't lose it!
|
|
|
|
var prevTowers = TD.custom_wave_data[TD.we_current_wave - 1].towers;
|
|
// Copy previous tower settings directly into the current wave's data
|
|
for (var t = 1; t <= 11; t++) {
|
|
TD.custom_wave_data[TD.we_current_wave].towers["tower_" + t] = prevTowers["tower_" + t] || "unlocked";
|
|
}
|
|
|
|
TD.we_loadCurrentWave(); // Instantly refresh UI to show the copied dropdowns
|
|
var d = document.getElementById("ui-action-desc");
|
|
if(d) d.innerText = "Tower settings copied from Wave " + (TD.we_current_wave - 1);
|
|
} else {
|
|
alert("There is no previous wave to copy from! (You are on Wave 1)");
|
|
}
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
TD.we_refreshWaveList();
|
|
|
|
|
|
// Build Tower List Framework (Only need to do this once)
|
|
var towerList = document.getElementById("we-tower-list");
|
|
if (towerList.children.length === 0) {
|
|
for (var t = 1; t <= 11; t++) {
|
|
var tName = "Tower " + t;
|
|
if (t === 11) tName = "Mr. Handy";
|
|
|
|
var row = document.createElement("div");
|
|
row.style.display = "flex"; row.style.justifyContent = "space-between"; row.style.alignItems = "center";
|
|
|
|
var lbl = document.createElement("label");
|
|
lbl.innerText = tName;
|
|
|
|
var sel = document.createElement("select");
|
|
sel.id = "we-tower-sel-" + t;
|
|
sel.style.background = "#222"; sel.style.color = "#0f0"; sel.style.border = "1px solid #0f0"; sel.style.padding = "2px";
|
|
sel.innerHTML = '<option value="unlocked">Unlocked</option><option value="locked">Locked</option><option value="hidden">Hidden</option>';
|
|
|
|
row.appendChild(lbl);
|
|
row.appendChild(sel);
|
|
towerList.appendChild(row);
|
|
}
|
|
}
|
|
|
|
TD.we_loadCurrentWave();
|
|
};
|
|
|
|
TD.we_loadCurrentWave = function() {
|
|
// Bulletproof safety fallback to prevent crashes if a map loaded incomplete data
|
|
if (!TD.custom_wave_data[TD.we_current_wave]) {
|
|
TD.custom_wave_data[TD.we_current_wave] = {
|
|
story_enable: false,
|
|
story_data: { "ALL": {title:"", desc:""}, "NCR": {title:"", desc:""}, "BOS": {title:"", desc:""}, "MINUTEMEN": {title:"", desc:""}, "RAIDERS": {title:"", desc:""} },
|
|
towers: {}, spawns: []
|
|
};
|
|
}
|
|
|
|
var data = TD.custom_wave_data[TD.we_current_wave];
|
|
|
|
// Backwards compatibility for data generated prior to the faction update
|
|
if (data.story_title !== undefined) {
|
|
data.story_data = {
|
|
"ALL": { title: data.story_title || "", desc: data.story_desc || "" },
|
|
"NCR": { title: "", desc: "" }, "BOS": { title: "", desc: "" }, "MINUTEMEN": { title: "", desc: "" }, "RAIDERS": { title: "", desc: "" }
|
|
};
|
|
delete data.story_title;
|
|
delete data.story_desc;
|
|
}
|
|
|
|
document.getElementById("we-story-enable").checked = data.story_enable;
|
|
|
|
|
|
var fData = data.story_data[TD.we_current_faction_tab] || { title: "", desc: "" };
|
|
document.getElementById("we-story-title").value = fData.title;
|
|
document.getElementById("we-story-desc").value = fData.desc;
|
|
|
|
for (var t = 1; t <= 11; t++) {
|
|
var sel = document.getElementById("we-tower-sel-" + t);
|
|
if (sel) sel.value = data.towers["tower_" + t] || "unlocked";
|
|
}
|
|
|
|
TD.we_renderSpawns();
|
|
};
|
|
|
|
TD.we_saveCurrentWave = function() {
|
|
var data = TD.custom_wave_data[TD.we_current_wave];
|
|
|
|
data.story_enable = document.getElementById("we-story-enable").checked;
|
|
|
|
// Save current faction tab text
|
|
if (!data.story_data) data.story_data = { "ALL": {title:"", desc:""}, "NCR": {title:"", desc:""}, "BOS": {title:"", desc:""}, "MINUTEMEN": {title:"", desc:""}, "RAIDERS": {title:"", desc:""} };
|
|
data.story_data[TD.we_current_faction_tab].title = document.getElementById("we-story-title").value;
|
|
data.story_data[TD.we_current_faction_tab].desc = document.getElementById("we-story-desc").value;
|
|
|
|
for (var t = 1; t <= 11; t++) {
|
|
var sel = document.getElementById("we-tower-sel-" + t);
|
|
if (sel) data.towers["tower_" + t] = sel.value;
|
|
}
|
|
};
|
|
|
|
TD.we_renderSpawns = function() {
|
|
var spawnList = document.getElementById("we-spawn-list");
|
|
spawnList.innerHTML = "";
|
|
var data = TD.custom_wave_data[TD.we_current_wave];
|
|
|
|
TD.lang.each(data.spawns, function(spawn, idx) {
|
|
var row = document.createElement("div");
|
|
row.style.display = "flex"; row.style.gap = "5px"; row.style.alignItems = "center"; row.style.background = "#222"; row.style.padding = "5px"; row.style.border = "1px solid #444";
|
|
|
|
var inpQty = document.createElement("input");
|
|
inpQty.type = "number"; inpQty.min = 1; inpQty.value = spawn.qty;
|
|
inpQty.style.width = "50px"; inpQty.style.background = "#111"; inpQty.style.color = "#0f0"; inpQty.style.border = "1px solid #0f0";
|
|
inpQty.onchange = function() { spawn.qty = parseInt(this.value); };
|
|
|
|
var selType = document.createElement("select");
|
|
selType.style.flex = 1; selType.style.background = "#111"; selType.style.color = "#0f0"; selType.style.border = "1px solid #0f0";
|
|
|
|
var totalMonsters = TD.getDefaultMonsterAttributes();
|
|
for (var m = 0; m < totalMonsters; m++) {
|
|
var mData = TD.getDefaultMonsterAttributes(m);
|
|
var opt = document.createElement("option");
|
|
opt.value = m; opt.innerText = mData.name;
|
|
selType.appendChild(opt);
|
|
}
|
|
selType.value = spawn.monster_idx;
|
|
selType.onchange = function() { spawn.monster_idx = parseInt(this.value); };
|
|
|
|
var btnDel = document.createElement("button");
|
|
btnDel.innerText = "X"; btnDel.style.background = "#500"; btnDel.style.color = "#f00"; btnDel.style.border = "1px solid #f00"; btnDel.style.cursor = "pointer";
|
|
btnDel.onclick = function() { data.spawns.splice(idx, 1); TD.we_renderSpawns(); };
|
|
|
|
row.appendChild(inpQty);
|
|
row.appendChild(document.createTextNode("x"));
|
|
row.appendChild(selType);
|
|
row.appendChild(btnDel);
|
|
spawnList.appendChild(row);
|
|
});
|
|
};
|
|
|
|
// --- END NEW LOGIC ---
|
|
|
|
// NEW: Floating Admin Cheat Window
|
|
|
|
// ==========================================
|
|
// NEW: ISOLATED SPECIAL EFFECTS MODULE
|
|
// ==========================================
|
|
TD.SpecialEffects = {
|
|
// Called once when an upgrade is purchased
|
|
onApply: function(building, effects) {
|
|
if (!effects) return;
|
|
building.has_special = true;
|
|
|
|
// Direct Stats
|
|
if (effects.damage_add) building.damage += effects.damage_add;
|
|
if (effects.range_add) {
|
|
building.range += effects.range_add;
|
|
building.range_px = building.range * TD.grid_size;
|
|
}
|
|
if (effects.damage_mult) building.damage = Math.floor(building.damage * effects.damage_mult);
|
|
if (effects.speed_mult) {
|
|
building.speed *= effects.speed_mult;
|
|
building._fire_wait2 = Math.floor(Math.max(TD.exp_fps / (building.speed * (TD.global_speed / 0.2)), 1));
|
|
}
|
|
|
|
// --- NEW: Flight Modifiers ---
|
|
if (effects.flight_speed_mult) building.flight_speed *= effects.flight_speed_mult;
|
|
if (effects.patrol_radius_add) building.flight_radius += (effects.patrol_radius_add * TD.grid_size);
|
|
|
|
if (effects.visual_type) {
|
|
// GAUSS RIFLE FIX: Keep the physical bullet logic but use a custom blue visual shot
|
|
if (effects.visual_type === "laser_gun" && effects.pierce) {
|
|
building.special_fire_pattern = "gauss";
|
|
} else {
|
|
building.visual_type = effects.visual_type;
|
|
}
|
|
}
|
|
|
|
// Combat Flags
|
|
if (effects.knockback) building.has_knockback = true;
|
|
if (effects.stun_chance) building.stun_chance = effects.stun_chance;
|
|
if (effects.pierce) building.pierce_count = effects.pierce;
|
|
if (effects.burst_count) building.burst_count = effects.burst_count;
|
|
if (effects.is_detector) building.is_detector = true;
|
|
|
|
// Firing Patterns
|
|
if (effects.fire_pattern) building.special_fire_pattern = effects.fire_pattern;
|
|
if (effects.pellet_count) building.special_pellet_count = effects.pellet_count;
|
|
|
|
// Special Active Abilities (Secondary weapons, Auras, etc.)
|
|
if (effects.secondary) { building.special_secondary = effects.secondary; building.special_secondary_cooldown = effects.secondary_cd || 5; }
|
|
if (effects.crit_req) { building.special_crit = true; building.special_crit_req = effects.crit_req; }
|
|
if (effects.dot_duration) { building.special_dot = true; building.special_dot_duration = effects.dot_duration; }
|
|
if (effects.chain_max) { building.special_chain = true; building.special_chain_max = effects.chain_max; }
|
|
|
|
// --- NEW: BONUS BOUNTY UPGRADE ---
|
|
if (effects.bonus_bounty) { building.bonus_bounty = true; }
|
|
|
|
// Dynamic Auras are now handled in TD.Building.prototype.step
|
|
if (effects.aura_type) {
|
|
building.aura_type = effects.aura_type;
|
|
}
|
|
|
|
// --- FIX: MISSING STAT MAPPINGS ---
|
|
if (effects.aura_vulnerability) building.aura_vulnerability = effects.aura_vulnerability;
|
|
if (effects.aura_slow) building.aura_slow = effects.aura_slow;
|
|
if (effects.aura_dot) building.aura_dot = effects.aura_dot;
|
|
if (effects.shatter_aoe) building.shatter_aoe = true;
|
|
if (effects.crit_chance) building.crit_chance = effects.crit_chance;
|
|
if (effects.crit_mult) building.crit_mult = effects.crit_mult;
|
|
if (effects.boss_damage_mult) building.boss_damage_mult = effects.boss_damage_mult;
|
|
},
|
|
|
|
// Called right before a bullet spawns
|
|
onFire: function(building) {
|
|
if (!building.special_fire_pattern) return false; // Use normal firing
|
|
|
|
var muzzle = building.muzzle || [building.cx, building.cy];
|
|
var cx = muzzle[0], cy = muzzle[1];
|
|
|
|
if (building.sound_type) TD.playSound(building.sound_type);
|
|
|
|
// GAUSS RIFLE: Fast physical blue bullet that tracks to target
|
|
if (building.special_fire_pattern === "gauss") {
|
|
new TD.Bullet(null, { building: building, damage: building.damage, target: building.target, speed: building.bullet_speed * 4.0, x: cx, y: cy, color: "#00aaff" });
|
|
return true;
|
|
}
|
|
|
|
|
|
if (building.special_fire_pattern === "flamer") {
|
|
var pellets = building.special_pellet_count || 5;
|
|
var spread = 45; // Wider cone spread
|
|
for (var p = 0; p < pellets; p++) {
|
|
var offset_x = (Math.random() * spread) - (spread / 2);
|
|
var offset_y = (Math.random() * spread) - (spread / 2);
|
|
|
|
var fake_target = {
|
|
cx: building.target.cx + offset_x,
|
|
cy: building.target.cy + offset_y,
|
|
is_valid: true,
|
|
is_fake: true
|
|
};
|
|
|
|
// Randomize speed and color for depth and texture
|
|
var rSpeed = building.bullet_speed * (0.5 + Math.random() * 0.7);
|
|
var colors = ["#ff5500", "#ffaa00", "#ff3300", "#ffff33"];
|
|
var rColor = colors[Math.floor(Math.random() * colors.length)];
|
|
|
|
new TD.Bullet(null, { building: building, damage: building.damage, target: fake_target, speed: rSpeed, x: cx, y: cy, color: rColor, is_flame: true, r: 4 });
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (building.special_fire_pattern === "shotgun") {
|
|
var pellets = building.special_pellet_count || 3;
|
|
// We create "fake" targets slightly offset from the real target to create a cone
|
|
var spread = 30; // Pixel spread
|
|
|
|
for (var p = 0; p < pellets; p++) {
|
|
var offset_x = (Math.random() * spread) - (spread / 2);
|
|
var offset_y = (Math.random() * spread) - (spread / 2);
|
|
|
|
var fake_target = {
|
|
cx: building.target.cx + offset_x,
|
|
cy: building.target.cy + offset_y,
|
|
is_valid: true,
|
|
is_fake: true
|
|
};
|
|
|
|
new TD.Bullet(null, { building: building, damage: building.damage, target: fake_target, speed: building.bullet_speed, x: cx, y: cy });
|
|
}
|
|
return true; // We handled the firing
|
|
}
|
|
|
|
if (building.special_fire_pattern === "twin") {
|
|
// Fire two bullets parallel to each other
|
|
var fake_target_1 = { cx: building.target.cx - 10, cy: building.target.cy - 10, is_valid: true, is_fake: true };
|
|
var fake_target_2 = { cx: building.target.cx + 10, cy: building.target.cy + 10, is_valid: true, is_fake: true };
|
|
|
|
new TD.Bullet(null, { building: building, damage: building.damage, target: fake_target_1, speed: building.bullet_speed, x: cx, y: cy });
|
|
new TD.Bullet(null, { building: building, damage: building.damage, target: fake_target_2, speed: building.bullet_speed, x: cx, y: cy });
|
|
return true;
|
|
}
|
|
|
|
if (building.special_fire_pattern === "mirv") {
|
|
// MIRV fires two mini-nukes
|
|
var fake_target_1 = { cx: building.target.cx - 30, cy: building.target.cy, is_valid: true, is_fake: true };
|
|
var fake_target_2 = { cx: building.target.cx + 30, cy: building.target.cy, is_valid: true, is_fake: true };
|
|
|
|
new TD.Bullet(null, { building: building, damage: building.damage, target: fake_target_1, speed: building.bullet_speed * 0.5, x: cx, y: cy });
|
|
new TD.Bullet(null, { building: building, damage: building.damage, target: fake_target_2, speed: building.bullet_speed * 0.5, x: cx, y: cy });
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
// Called every frame by the tower
|
|
onStep: function(building) {
|
|
if (!building || !building.is_valid || building.is_pre_building) return;
|
|
|
|
// Secondary Weapons (Grenades, Nukes, etc)
|
|
if (building.special_secondary) {
|
|
if (typeof building._sec_timer === 'undefined') building._sec_timer = 0;
|
|
building._sec_timer++;
|
|
|
|
var triggerTime = TD.exp_fps * (building.special_secondary_cooldown || 5);
|
|
|
|
// Some abilities trigger even without a target (like dropping napalm while flying)
|
|
var needsTarget = ["grenade", "nuke", "gas_grenade", "barricade_trap", "heavy_bomb"].includes(building.special_secondary);
|
|
var canFire = needsTarget ? (building.target && building.target.is_valid) : true;
|
|
|
|
if (building._sec_timer >= triggerTime && canFire) {
|
|
building._sec_timer = 0;
|
|
|
|
if (building.special_secondary === "grenade") {
|
|
var blast = TD.grid_size * 1.5;
|
|
TD.Explode("grenade-" + TD.lang.rndStr(), { cx: building.target.cx, cy: building.target.cy, r: blast / _TD.retina, step_level: 1, render_level: 9, color: "#ff8800", scene: building.map.scene, time: 0.3 });
|
|
|
|
var _b = building;
|
|
TD.lang.each(building.map.monsters, function(m) {
|
|
if (m.is_valid && Math.pow(m.cx - _b.target.cx, 2) + Math.pow(m.cy - _b.target.cy, 2) <= Math.pow(blast, 2)) m.beHit(_b, Math.floor(_b.damage * 0.5));
|
|
});
|
|
}
|
|
else if (building.special_secondary === "nuke") {
|
|
var blast = TD.grid_size * 5.0;
|
|
TD.Explode("mini-nuke-" + TD.lang.rndStr(), { cx: building.target.cx, cy: building.target.cy, r: blast / _TD.retina, step_level: 1, render_level: 9, color: "#ffff00", scene: building.map.scene, time: 1.0 });
|
|
var _b = building;
|
|
TD.lang.each(building.map.monsters, function(m) {
|
|
if (m.is_valid && Math.pow(m.cx - _b.target.cx, 2) + Math.pow(m.cy - _b.target.cy, 2) <= Math.pow(blast, 2)) m.beHit(_b, 1000);
|
|
});
|
|
}
|
|
else if (building.special_secondary === "liberty_laser") {
|
|
var _b = building;
|
|
TD.lang.each(building.map.monsters, function(m) {
|
|
if (m.is_valid && Math.pow(m.cx - _b.cx, 2) + Math.pow(m.cy - _b.cy, 2) <= Math.pow(_b.range_px, 2)) {
|
|
m.beHit(_b, 800);
|
|
TD.Explode("lib-hit-" + TD.lang.rndStr(), { cx: m.cx, cy: m.cy, r: 15 * _TD.retina, step_level: 1, render_level: 9, color: "#00aaff", scene: _b.map.scene, time: 0.2 });
|
|
}
|
|
});
|
|
}
|
|
else if (building.special_secondary === "emp") {
|
|
var _b = building;
|
|
TD.Explode("emp-pulse-" + TD.lang.rndStr(), { cx: _b.cx, cy: _b.cy, r: _b.range_px / _TD.retina, step_level: 1, render_level: 9, color: "#88ccff", scene: _b.map.scene, time: 0.5 });
|
|
TD.lang.each(building.map.monsters, function(m) {
|
|
if (m.is_valid && Math.pow(m.cx - _b.cx, 2) + Math.pow(m.cy - _b.cy, 2) <= Math.pow(_b.range_px, 2)) {
|
|
if (!m.is_frozen) m.original_speed = m.speed;
|
|
m.is_frozen = true;
|
|
m.speed = 0.1;
|
|
m.freeze_timer = TD.exp_fps * 1.0; // 1 second stun
|
|
}
|
|
});
|
|
}
|
|
else if (building.special_secondary === "scramble") {
|
|
var _b = building;
|
|
TD.Explode("scramble-pulse-" + TD.lang.rndStr(), { cx: _b.cx, cy: _b.cy, r: _b.range_px / _TD.retina, step_level: 1, render_level: 9, color: "#ff55ff", scene: _b.map.scene, time: 0.5 });
|
|
TD.lang.each(building.map.monsters, function(m) {
|
|
if (m.is_valid && Math.pow(m.cx - _b.cx, 2) + Math.pow(m.cy - _b.cy, 2) <= Math.pow(_b.range_px, 2)) {
|
|
// Invert their target grid logic so they walk backwards
|
|
var temp = m.grid;
|
|
m.grid = m.next_grid;
|
|
m.next_grid = temp;
|
|
m.freeze_timer = TD.exp_fps * 1.5; // Acts as a confusion timer before they recalculate
|
|
}
|
|
});
|
|
}
|
|
else if (building.special_secondary === "gas_grenade") {
|
|
var blast = TD.grid_size * 2.0;
|
|
TD.Explode("gas-" + TD.lang.rndStr(), { cx: building.target.cx, cy: building.target.cy, r: blast / _TD.retina, step_level: 1, render_level: 9, color: "#55ff55", scene: building.map.scene, time: 3.0 });
|
|
var _b = building;
|
|
TD.lang.each(building.map.monsters, function(m) {
|
|
if (m.is_valid && Math.pow(m.cx - _b.target.cx, 2) + Math.pow(m.cy - _b.target.cy, 2) <= Math.pow(blast, 2)) {
|
|
m.is_slowed = true;
|
|
m.slow_timer = TD.exp_fps * 3; // Slowed for 3 seconds
|
|
}
|
|
});
|
|
}
|
|
else if (building.special_secondary === "napalm_drop") {
|
|
// Drops fire directly beneath the flying Vertibird
|
|
new TD.RadZone("napalm-" + TD.lang.rndStr(), { map: building.map, cx: building.cx, cy: building.cy });
|
|
}
|
|
else if (building.special_secondary === "heavy_bomb") {
|
|
// Finds the leader and drops a nuke-sized explosion that only deals 300 damage
|
|
var leader = null;
|
|
var shortest_way = 999999;
|
|
TD.lang.each(building.map.monsters, function(m) {
|
|
if (m.is_valid && m.way.length < shortest_way) {
|
|
shortest_way = m.way.length;
|
|
leader = m;
|
|
}
|
|
});
|
|
|
|
if (leader) {
|
|
var blast = TD.grid_size * 3.0;
|
|
TD.playSound("snd_explosion");
|
|
TD.Explode("bomb-" + TD.lang.rndStr(), { cx: leader.cx, cy: leader.cy, r: blast / _TD.retina, step_level: 1, render_level: 9, color: "#ff5500", scene: building.map.scene, time: 0.6 });
|
|
var _b = building;
|
|
TD.lang.each(building.map.monsters, function(m) {
|
|
if (m.is_valid && Math.pow(m.cx - leader.cx, 2) + Math.pow(m.cy - leader.cy, 2) <= Math.pow(blast, 2)) m.beHit(_b, 300);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// Called when a bullet hits a monster. Returns 'false' if the bullet should keep flying (Pierce).
|
|
onHit: function(bullet, monster) {
|
|
var bld = bullet.building;
|
|
if (!bld || !bld.is_valid) return true;
|
|
|
|
// --- NEW: RNG Crits ---
|
|
if (bld.crit_chance && Math.random() <= bld.crit_chance) {
|
|
var mult = bld.crit_mult || 2.0;
|
|
bullet.damage = Math.floor(bullet.damage * mult);
|
|
new TD.FloatingText("crit-" + TD.lang.rndStr(), { cx: monster.cx, cy: monster.cy - 15, text: "CRIT!", color: "#ffaa00", map: bld.map, size: 12, life: 24 });
|
|
}
|
|
|
|
// --- NEW: Boss Damage Multiplier ---
|
|
if (bld.boss_damage_mult && monster.life0 >= 2000) {
|
|
bullet.damage = Math.floor(bullet.damage * bld.boss_damage_mult);
|
|
}
|
|
|
|
// 1. Stun Chance
|
|
if (bld.stun_chance) {
|
|
|
|
if (Math.random() <= bld.stun_chance) {
|
|
if (!monster.is_frozen) {
|
|
monster.is_frozen = true;
|
|
monster.original_speed = monster.speed;
|
|
monster.speed = 0.1;
|
|
}
|
|
monster.freeze_timer = TD.exp_fps * 1.5;
|
|
TD.Explode("stun-" + TD.lang.rndStr(), { cx: monster.cx, cy: monster.cy, r: 15 * _TD.retina, step_level: 1, render_level: 9, color: "#ffff00", scene: bld.map.scene, time: 0.2 });
|
|
}
|
|
}
|
|
|
|
// 2. Knockback (MULTIPLIER INCREASED TO 5.0)
|
|
if (bld.has_knockback && monster.life0 < 2000) {
|
|
monster.cx -= (bullet.vx * 5.0);
|
|
monster.cy -= (bullet.vy * 5.0);
|
|
monster.caculatePos();
|
|
}
|
|
|
|
// 3. Guaranteed Crits
|
|
if (bld.special_crit) {
|
|
if (typeof bld._shot_tracker === 'undefined') bld._shot_tracker = 0;
|
|
bld._shot_tracker++;
|
|
if (bld._shot_tracker >= bld.special_crit_req) {
|
|
bld._shot_tracker = 0;
|
|
bullet.damage *= 5;
|
|
new TD.FloatingText("crit-" + TD.lang.rndStr(), { cx: monster.cx, cy: monster.cy - 15, text: "CRIT!", color: "#ff0000", map: bld.map, size: 14, life: 24 });
|
|
TD.Explode("crit-blast-" + TD.lang.rndStr(), { cx: monster.cx, cy: monster.cy, r: 15 * _TD.retina, step_level: 1, render_level: 9, color: "#ff0000", scene: bld.map.scene, time: 0.2 });
|
|
}
|
|
}
|
|
|
|
// 4. DoTs
|
|
if (bld.special_dot) {
|
|
monster.is_poisoned = true;
|
|
monster.poison_timer = TD.exp_fps * (bld.special_dot_duration || 2);
|
|
monster.poison_damage = Math.floor(bullet.damage * 0.2); // 20% of bullet damage per tick
|
|
if (monster.poison_damage < 1) monster.poison_damage = 1;
|
|
TD.Explode("fire-" + TD.lang.rndStr(), { cx: monster.cx, cy: monster.cy, r: monster.r * 1.5, step_level: 1, render_level: 9, color: "#ff8800", scene: bld.map.scene, time: 0.2 });
|
|
}
|
|
|
|
// 5. Chain Lightning
|
|
if (bld.special_chain) {
|
|
var hit_count = 0;
|
|
TD.lang.each(bld.map.monsters, function(m) {
|
|
if (m.is_valid && m !== monster && hit_count < bld.special_chain_max) {
|
|
if (Math.pow(m.cx - monster.cx, 2) + Math.pow(m.cy - monster.cy, 2) <= Math.pow(TD.grid_size * 2.5, 2)) {
|
|
m.beHit(bld, Math.floor(bullet.damage * 0.5));
|
|
var ctx = TD.ctx;
|
|
ctx.lineWidth = 2 * _TD.retina; ctx.strokeStyle = "rgba(0, 255, 255, 0.8)";
|
|
ctx.beginPath(); ctx.moveTo(monster.cx, monster.cy); ctx.lineTo(m.cx, m.cy); ctx.stroke();
|
|
hit_count++;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 6. Pierce Logic (Returns false so bullet doesn't die)
|
|
if (bullet.pierce_count && bullet.pierce_count > 1) {
|
|
bullet.pierce_count--;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
});
|
|
|
|
|
|
// NEW: Global Tooltip Mouse Tracker
|
|
|
|
document.addEventListener("mousemove", function(e) {
|
|
var tooltip = document.getElementById("tower-tooltip");
|
|
if (tooltip && tooltip.style.display === "block") {
|
|
var x = e.pageX + 15;
|
|
var y = e.pageY + 15;
|
|
|
|
// Bounding Box Logic: Stop it from bleeding off the right or bottom edges
|
|
var tw = tooltip.offsetWidth;
|
|
var th = tooltip.offsetHeight;
|
|
|
|
if (x + tw > window.innerWidth) { x = e.pageX - tw - 15; }
|
|
if (y + th > window.innerHeight) { y = window.innerHeight - th - 5; }
|
|
|
|
tooltip.style.left = x + "px";
|
|
tooltip.style.top = y + "px";
|
|
}
|
|
});
|
|
|
|
// NEW: Reusable Panel Ignition Function
|
|
window.triggerPanelIgnition = function() {
|
|
document.getElementById("ui-money-container").innerHTML = "";
|
|
document.getElementById("ui-score-container").innerHTML = "";
|
|
document.getElementById("ui-life-container").innerHTML = "";
|
|
document.getElementById("ui-wave-container").innerHTML = "";
|
|
var btns = document.querySelectorAll(".cmd-btn, .sys-btn");
|
|
for (var i = 0; i < btns.length; i++) {
|
|
var btn = btns[i];
|
|
// Only ignite if the button is NOT locked or manually turned off
|
|
if (!btn.disabled && !btn.classList.contains("unlit")) {
|
|
btn.classList.remove("warm-up-1", "warm-up-2", "warm-up-3", "warm-up-4", "warm-up-5");
|
|
void btn.offsetWidth; // Force CSS reflow to restart the animation
|
|
btn.classList.add("warm-up-" + (Math.floor(Math.random() * 5) + 1));
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
window.onload = function () { // --- DEMO MODE INIT ---
|
|
var board = document.getElementById("td-board");
|
|
|
|
board.style.display = "block";
|
|
board.classList.add("demo-blur");
|
|
|
|
// Hide UI elements during demo
|
|
var controlPanel = document.getElementById("control-panel");
|
|
if (controlPanel) controlPanel.style.display = "none";
|
|
var topBar = document.getElementById("top-bar");
|
|
if (topBar) topBar.style.display = "none";
|
|
|
|
window.selectedLevelName = "demo_map_1";
|
|
window.is_demo_mode = true;
|
|
|
|
// Expose TD to the window so the start button can restart the engine
|
|
_TD.a.push(function(TD) { window.TD = TD; });
|
|
_TD.init("td-board", true);
|
|
|
|
// --- NEW MAIN MENU LOGIC ---
|
|
|
|
|
|
var gameTypeSelect = document.getElementById("gametype-select");
|
|
var mapGroup = document.getElementById("map-selection-group");
|
|
var specialGroup = document.getElementById("special-selection-group");
|
|
|
|
var updatePlayButtonLighting = function() {
|
|
var btnStart = document.getElementById("btn-start");
|
|
var isEndless = gameTypeSelect && gameTypeSelect.value === "endless";
|
|
var checkedCount = document.querySelectorAll(".special-chk:checked").length;
|
|
|
|
if (btnStart) {
|
|
if (isEndless || checkedCount === 3) {
|
|
btnStart.classList.add("btn-lit");
|
|
} else {
|
|
btnStart.classList.remove("btn-lit");
|
|
}
|
|
}
|
|
};
|
|
|
|
// 1. Define the isolated dynamic lighting logic
|
|
var updatePlayButtonLighting = function() {
|
|
var btnStart = document.getElementById("btn-start");
|
|
var isCampaign = gameTypeSelect && gameTypeSelect.value === "campaign";
|
|
var checkedCount = document.querySelectorAll(".special-chk:checked").length;
|
|
|
|
if (btnStart) {
|
|
// Unlock if NOT campaign, OR if campaign has 3 perks selected
|
|
if (!isCampaign || checkedCount === 3) {
|
|
if (!btnStart.classList.contains("menu-play-ready")) {
|
|
btnStart.classList.add("menu-play-ready");
|
|
btnStart.disabled = false;
|
|
btnStart.style.cursor = "pointer";
|
|
}
|
|
} else {
|
|
btnStart.classList.remove("menu-play-ready");
|
|
btnStart.disabled = true;
|
|
btnStart.style.cursor = "not-allowed";
|
|
}
|
|
}
|
|
};
|
|
|
|
// 2. Bind the dropdown change listener
|
|
if (gameTypeSelect) {
|
|
gameTypeSelect.addEventListener("change", function() {
|
|
if (this.value === "campaign") {
|
|
mapGroup.style.display = "none"; specialGroup.style.display = "block";
|
|
} else if (this.value === "custom") {
|
|
mapGroup.style.display = "none"; specialGroup.style.display = "none"; // Blank Slate
|
|
} else {
|
|
mapGroup.style.display = "block"; specialGroup.style.display = "none";
|
|
}
|
|
updatePlayButtonLighting();
|
|
});
|
|
}
|
|
|
|
// 3. Bind the S.P.E.C.I.A.L. checkbox listeners
|
|
var specialCheckboxes = document.querySelectorAll(".special-chk");
|
|
for (var i = 0; i < specialCheckboxes.length; i++) {
|
|
specialCheckboxes[i].addEventListener("change", function() {
|
|
var checkedCount = document.querySelectorAll(".special-chk:checked").length;
|
|
for (var j = 0; j < specialCheckboxes.length; j++) {
|
|
var box = specialCheckboxes[j];
|
|
if (checkedCount >= 3) {
|
|
if (!box.checked) box.disabled = true;
|
|
} else {
|
|
box.disabled = false;
|
|
}
|
|
}
|
|
updatePlayButtonLighting();
|
|
});
|
|
}
|
|
|
|
// 4. Set initial lighting state on load
|
|
updatePlayButtonLighting();
|
|
|
|
// --- NEW: Wasteland Guide Logic ---
|
|
var guideIcon = document.getElementById("guide-icon");
|
|
if (guideIcon) {
|
|
guideIcon.onclick = function() {
|
|
document.getElementById("guide-modal-overlay").style.display = "flex";
|
|
|
|
// Auto-populate the monster list on the right page
|
|
var listContainer = document.getElementById("guide-monster-list");
|
|
if (listContainer && TD.getDefaultMonsterAttributes) {
|
|
listContainer.innerHTML = ""; // clear it
|
|
var totalMonsters = TD.getDefaultMonsterAttributes(); // gets the count
|
|
|
|
for (var i = 0; i < totalMonsters; i++) {
|
|
var mData = TD.getDefaultMonsterAttributes(i);
|
|
var html = "<div style='margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px dashed rgba(0,0,0,0.2);'>";
|
|
|
|
var tags = [];
|
|
if (mData.is_flying) tags.push("<strong>[FLYING]</strong>");
|
|
if (mData.is_stealthed) tags.push("<strong>[STEALTH]</strong>");
|
|
if (mData.speed >= 15) tags.push("[FAST]");
|
|
if (mData.life >= 500) tags.push("<strong>[VERY STRONG]</strong>");
|
|
if (mData.life >= 2000) tags.push("<strong style='color:#800;'>[BOSS]</strong>");
|
|
|
|
html += "<h3>" + mData.name.toUpperCase() + "</h3> " + tags.join(" ") + "<br>";
|
|
html += "<strong>HP:</strong> " + mData.life + " | <strong>Speed:</strong> " + mData.speed + " | <strong>Dmg:</strong> " + mData.damage + "<br>";
|
|
html += "<em style='color: #444;'>" + (mData.desc || "A dangerous creature of the wastes.") + "</em>";
|
|
html += "</div>";
|
|
|
|
listContainer.innerHTML += html;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
// --- END NEW MAIN MENU LOGIC ---
|
|
|
|
|
|
|
|
// 2. Bind the dropdown change listener
|
|
if (gameTypeSelect) {
|
|
gameTypeSelect.addEventListener("change", function() {
|
|
if (this.value === "campaign") {
|
|
mapGroup.style.display = "none";
|
|
specialGroup.style.display = "block";
|
|
} else {
|
|
mapGroup.style.display = "block";
|
|
specialGroup.style.display = "none";
|
|
}
|
|
updatePlayButtonLighting();
|
|
});
|
|
}
|
|
|
|
// 3. Bind the S.P.E.C.I.A.L. checkbox listeners
|
|
var specialCheckboxes = document.querySelectorAll(".special-chk");
|
|
for (var i = 0; i < specialCheckboxes.length; i++) {
|
|
specialCheckboxes[i].addEventListener("change", function() {
|
|
var checkedCount = document.querySelectorAll(".special-chk:checked").length;
|
|
for (var j = 0; j < specialCheckboxes.length; j++) {
|
|
var box = specialCheckboxes[j];
|
|
if (checkedCount >= 3) {
|
|
if (!box.checked) box.disabled = true;
|
|
} else {
|
|
box.disabled = false;
|
|
}
|
|
}
|
|
updatePlayButtonLighting();
|
|
});
|
|
}
|
|
|
|
|
|
// Dynamically generate the Admin Dropdown menu by reading the internal StageData
|
|
var populateAdminDropdown = function() {
|
|
var adminSelect = document.getElementById("admin-level-select");
|
|
if (!adminSelect) return;
|
|
|
|
// Scan the entire document's source code for the map definitions
|
|
var dummyData = document.documentElement.innerHTML;
|
|
|
|
// Look for scene_level, campaign_map, and demo_map strings
|
|
var sceneNames = dummyData.match(/scene_level_\d+/g) || [];
|
|
var campaignNames = dummyData.match(/campaign_map_\d+/g) || [];
|
|
var demoNames = dummyData.match(/demo_map_\d+/g) || [];
|
|
var levelNames = sceneNames.concat(campaignNames, demoNames);
|
|
|
|
// Force all 16 campaign maps to always exist in the dropdown so you can build and export them
|
|
for (var c = 1; c <= 16; c++) {
|
|
levelNames.push("campaign_map_" + c);
|
|
}
|
|
|
|
if (levelNames.length === 0) levelNames = ["scene_level_1", "scene_level_2"];
|
|
|
|
// Remove duplicates
|
|
levelNames = levelNames.filter(function(item, pos) { return levelNames.indexOf(item) == pos; });
|
|
|
|
adminSelect.innerHTML = "";
|
|
for(var i=0; i<levelNames.length; i++) {
|
|
var opt = document.createElement("option");
|
|
opt.value = levelNames[i];
|
|
opt.innerText = levelNames[i].toUpperCase();
|
|
adminSelect.appendChild(opt);
|
|
}
|
|
};
|
|
populateAdminDropdown(); // Run the scan when the page loads
|
|
|
|
var populateAdminTowers = function() {
|
|
var sel = document.getElementById("admin-tower-select");
|
|
if (!sel || !TD.factionData) return;
|
|
sel.innerHTML = "";
|
|
|
|
var grpAdap = document.createElement("optgroup");
|
|
grpAdap.label = "ADAPTIVE (Matches Player Faction)";
|
|
for (var i = 1; i <= 11; i++) {
|
|
var opt = document.createElement("option");
|
|
opt.value = "tower_" + i;
|
|
opt.innerText = "Slot " + i + (i === 11 ? " (Mr. Handy)" : "");
|
|
grpAdap.appendChild(opt);
|
|
}
|
|
sel.appendChild(grpAdap);
|
|
|
|
for (var f in TD.factionData) {
|
|
if (TD.factionData.hasOwnProperty(f)) {
|
|
var grp = document.createElement("optgroup");
|
|
grp.label = f + " SPECIFIC";
|
|
var list = TD.factionData[f];
|
|
for (var j = 0; j < list.length; j++) {
|
|
var t = list[j];
|
|
var opt = document.createElement("option");
|
|
opt.value = t.id;
|
|
opt.innerText = t.name;
|
|
grp.appendChild(opt);
|
|
}
|
|
sel.appendChild(grp);
|
|
}
|
|
}
|
|
};
|
|
populateAdminTowers();
|
|
|
|
|
|
// NEW: Reusable panel Ignition Function
|
|
window.triggerPanelIgnition = function() {
|
|
// Clear Nixie containers to force a rebuild
|
|
var containers = ["ui-money-container", "ui-score-container", "ui-life-container", "ui-wave-container"];
|
|
for (var i = 0; i < containers.length; i++) {
|
|
var c = document.getElementById(containers[i]);
|
|
if (c) c.innerHTML = "";
|
|
}
|
|
|
|
// Apply random flicker profiles to ENABLED buttons only
|
|
var buttons = document.querySelectorAll(".cmd-btn, .sys-btn");
|
|
for (var j = 0; j < buttons.length; j++) {
|
|
var btn = buttons[j];
|
|
// Clear memory on full system reboot
|
|
btn.removeAttribute("data-warmed-up");
|
|
// Strip out any old warm-up classes first
|
|
btn.className = btn.className.replace(/warm-up-[\w-]+/g, "").trim();
|
|
|
|
// Only ignite if the button is physically enabled and NOT locked
|
|
if (!btn.disabled && !btn.classList.contains("unlit")) {
|
|
void btn.offsetWidth; // Force CSS reflow
|
|
var roll = Math.random() * 100;
|
|
if (roll > 20) {
|
|
// 80%: Instant on
|
|
} else if (roll > 5) {
|
|
// 15%: Random jagged flicker
|
|
btn.classList.add("warm-up-" + (Math.floor(Math.random() * 5) + 1));
|
|
} else {
|
|
// 5%: Smooth fade
|
|
btn.classList.add("warm-up-fade");
|
|
}
|
|
btn.setAttribute("data-warmed-up", "true"); // Tag it
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
var btnDeploy = document.getElementById("campaign-btn-deploy");
|
|
if (btnDeploy) {
|
|
btnDeploy.addEventListener("click", function() {
|
|
document.getElementById("campaign-intro-modal-overlay").style.display = "none";
|
|
|
|
// Release the deep pause and JUMPSTART the engine loop
|
|
TD.is_paused = false;
|
|
if (typeof TD.step === 'function') TD.step(); // <--- This wakes the clock back up!
|
|
|
|
if (TD.stage && TD.stage.current_act && TD.stage.current_act.current_scene) {
|
|
TD.stage.current_act.current_scene.start();
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
// --- NEW: Watcher for buttons gaining/losing power ---
|
|
var origDisabled = Object.getOwnPropertyDescriptor(HTMLButtonElement.prototype, 'disabled');
|
|
if (origDisabled) {
|
|
Object.defineProperty(HTMLButtonElement.prototype, 'disabled', {
|
|
get: function() { return origDisabled.get.call(this); },
|
|
set: function(val) {
|
|
origDisabled.set.call(this, val);
|
|
if (val) this.setAttribute('disabled', 'disabled');
|
|
else this.removeAttribute('disabled');
|
|
}
|
|
});
|
|
}
|
|
|
|
var panelObserver = new MutationObserver(function(mutations) {
|
|
mutations.forEach(function(mutation) {
|
|
if (mutation.attributeName === "disabled") {
|
|
var btn = mutation.target;
|
|
if (btn.classList.contains("cmd-btn") || btn.classList.contains("sys-btn")) {
|
|
btn.className = btn.className.replace(/warm-up-[\w-]+/g, "").trim(); // Always clear old flickers
|
|
|
|
// If it just gained power, ignite it
|
|
if (!btn.disabled && !btn.classList.contains("unlit")) {
|
|
void btn.offsetWidth;
|
|
// Only run RNG if it hasn't warmed up yet!
|
|
if (!btn.getAttribute("data-warmed-up")) {
|
|
var roll = Math.random() * 100;
|
|
if (roll > 20) {
|
|
// 80%: Instant on
|
|
} else if (roll > 5) {
|
|
// 15%: Flicker
|
|
btn.classList.add("warm-up-" + (Math.floor(Math.random() * 5) + 1));
|
|
} else {
|
|
// 5%: Fade
|
|
btn.classList.add("warm-up-fade");
|
|
}
|
|
btn.setAttribute("data-warmed-up", "true");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
panelObserver.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['disabled'] });
|
|
// ------------------------------------------------
|
|
|
|
|
|
document.getElementById("btn-start").addEventListener("click", function() {
|
|
// Capture Game Type
|
|
var gameType = document.getElementById("gametype-select");
|
|
window.is_campaign_mode = (gameType && gameType.value === "campaign");
|
|
window.is_custom_mode = (gameType && gameType.value === "custom");
|
|
window.is_assault_mode = (gameType && gameType.value === "assault");
|
|
|
|
|
|
|
|
// Capture S.P.E.C.I.A.L. choices
|
|
|
|
window.selectedSPECIALs = [];
|
|
var checkedSpecials = document.querySelectorAll(".special-chk:checked");
|
|
for (var i = 0; i < checkedSpecials.length; i++) {
|
|
window.selectedSPECIALs.push(checkedSpecials[i].value);
|
|
}
|
|
|
|
// Map Selection Logic
|
|
if (window.is_campaign_mode) {
|
|
window.selectedLevelName = "campaign_map_1"; // Default start for campaign
|
|
window.campaign_map_index = 1; // Explicitly set the index!
|
|
|
|
} else {
|
|
window.selectedLevelName = document.getElementById("level-select").value;
|
|
}
|
|
|
|
window.selectedFaction = document.getElementById("faction-select").value;
|
|
|
|
window.customDifficulty = parseFloat(document.getElementById("difficulty-select").value);
|
|
|
|
document.getElementById("start-screen").style.display = "none";
|
|
document.getElementById("top-bar").style.display = "flex";
|
|
|
|
var board = document.getElementById("td-board");
|
|
board.style.display = "block";
|
|
board.style.transition = "filter 0.5s ease"; // Add fade transition only for un-blurring
|
|
board.classList.remove("demo-blur");
|
|
|
|
document.getElementById("control-panel").style.display = "flex";
|
|
|
|
// Stop demo and start real game
|
|
window.is_demo_mode = false;
|
|
TD.stage.clear();
|
|
window.triggerPanelIgnition();
|
|
TD.total_shots = 0; // Reset shots for new game
|
|
TD.start();
|
|
|
|
// Start the battle music!
|
|
|
|
if (typeof TD.playMusic === 'function') {
|
|
TD.playMusic("bgm_game");
|
|
}
|
|
|
|
// --- NEW: Global Starting Caps Doubler ---
|
|
if (typeof TD.money !== "undefined") {
|
|
TD.money = Math.floor(TD.money * 2.0);
|
|
}
|
|
|
|
// Apply Global Passives (Endurance, Agility Items)
|
|
if (TD.perks) {
|
|
if (TD.perks.E) { TD.life = Math.floor(TD.life * 1.5); TD.base_health = Math.floor(TD.base_health * 1.5); }
|
|
if (TD.perks.A) TD.getCooldownFrames = function() { var base = (TD.difficulty < 1) ? 24 : (TD.difficulty > 1) ? 288 : 120; return Math.floor(base * 0.85); };
|
|
}
|
|
});
|
|
|
|
// --- NEW: LOAD GAME LOGIC ---
|
|
var btnLoadGame = document.getElementById("btn-load-game");
|
|
var inputLoadGame = document.getElementById("input-load-game");
|
|
|
|
if (btnLoadGame && inputLoadGame) {
|
|
btnLoadGame.addEventListener("click", function() {
|
|
inputLoadGame.click(); // Trigger the hidden file input
|
|
});
|
|
|
|
inputLoadGame.addEventListener("change", function(e) {
|
|
var file = e.target.files[0];
|
|
if (!file) return;
|
|
var reader = new FileReader();
|
|
reader.onload = function(evt) {
|
|
try {
|
|
var saveData = JSON.parse(evt.target.result);
|
|
|
|
// 1. Transition UI
|
|
document.getElementById("start-screen").style.display = "none";
|
|
document.getElementById("top-bar").style.display = "flex";
|
|
var board = document.getElementById("td-board");
|
|
board.style.display = "block";
|
|
board.classList.remove("demo-blur");
|
|
document.getElementById("control-panel").style.display = "flex";
|
|
|
|
// 2. Set Global Configurations
|
|
window.is_demo_mode = false;
|
|
window.selectedSPECIALs = saveData.specials || [];
|
|
window.selectedFaction = saveData.faction || "NCR";
|
|
TD.current_faction = window.selectedFaction;
|
|
TD.difficulty = saveData.difficulty || 1.0;
|
|
|
|
// 3. Start Engine
|
|
TD.stage.clear();
|
|
window.triggerPanelIgnition();
|
|
TD.start();
|
|
|
|
// 4. Inject Saved Stats
|
|
TD.money = saveData.money || 0;
|
|
TD.score = saveData.score || 0;
|
|
|
|
if (TD.stage && TD.stage.current_act && TD.stage.current_act.current_scene) {
|
|
TD.stage.current_act.current_scene.wave = saveData.wave || 0;
|
|
}
|
|
|
|
// 5. Restore Unlocked Towers
|
|
if (TD.factionData && TD.factionData[TD.current_faction] && saveData.towers_unlocked) {
|
|
for (var i = 0; i < saveData.towers_unlocked.length; i++) {
|
|
if (TD.factionData[TD.current_faction][i]) {
|
|
TD.factionData[TD.current_faction][i].is_unlocked = saveData.towers_unlocked[i];
|
|
}
|
|
}
|
|
}
|
|
TD.loadFaction(TD.current_faction);
|
|
|
|
// Start the battle music!
|
|
if (typeof TD.playMusic === 'function') {
|
|
TD.playMusic("bgm_game");
|
|
}
|
|
|
|
} catch(err) {
|
|
alert("ERROR: Save file corrupted or invalid JSON.");
|
|
console.error(err);
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
});
|
|
}
|
|
|
|
// --- NEW: CAMPAIGN PROCEED BUTTON LOGIC ---
|
|
|
|
|
|
var btnProceed = document.getElementById("campaign-btn-proceed");
|
|
if (btnProceed) {
|
|
btnProceed.addEventListener("click", function() {
|
|
if (window.campaign_map_index >= 16) {
|
|
window.location.reload(); // Game won, back to main menu
|
|
return;
|
|
}
|
|
|
|
// Chapter Finale Check
|
|
if (window.campaign_map_index % 4 === 0) {
|
|
// Save S.P.E.C.I.A.L. Perk
|
|
var chosenPerk = document.getElementById("campaign-special-select").value;
|
|
if (window.selectedSPECIALs.indexOf(chosenPerk) === -1) {
|
|
window.selectedSPECIALs.push(chosenPerk);
|
|
}
|
|
|
|
// Relock Towers (Keep first 4 base towers unlocked)
|
|
if (TD.factionData && TD.current_faction) {
|
|
var towers = TD.factionData[TD.current_faction];
|
|
for (var i = 0; i < towers.length; i++) {
|
|
towers[i].is_unlocked = (i < 4);
|
|
}
|
|
TD.loadFaction(TD.current_faction);
|
|
}
|
|
}
|
|
|
|
var m = TD.stage.current_act.current_scene.map;
|
|
|
|
// 1. Wipe the board
|
|
TD.lang.each(m.buildings, function(b) { b.grid.removeBuilding(); }); m.buildings = [];
|
|
TD.lang.each(m.monsters, function(mob) { mob.is_valid = false; }); m.monsters = [];
|
|
TD.lang.each(m.bullets, function(bul) { bul.is_valid = false; }); m.bullets = [];
|
|
m.has_weapon = false;
|
|
|
|
// 2. Load next map
|
|
window.campaign_map_index++;
|
|
var nextLevel = "campaign_map_" + window.campaign_map_index;
|
|
|
|
var cfg = TD.getDefaultStageData(nextLevel);
|
|
|
|
if (cfg && cfg.map) {
|
|
if (cfg.bg_image) document.getElementById("td-board").style.backgroundImage = "url('" + cfg.bg_image + "')";
|
|
m.map_type = cfg.map.map_type || "traditional";
|
|
|
|
// Reset grids
|
|
TD.lang.each(m.grids, function(g) { g.passable_flag = 1; g.build_flag = 1; g.is_entrance = false; g.is_exit = false; });
|
|
|
|
// Apply new grid config
|
|
if (cfg.map.grids_cfg) {
|
|
TD.lang.each(cfg.map.grids_cfg, function (obj) {
|
|
var grid = m.getGrid(obj.pos[0], obj.pos[1]);
|
|
if (grid) {
|
|
if (!isNaN(obj.passable_flag)) grid.passable_flag = obj.passable_flag;
|
|
if (!isNaN(obj.build_flag)) grid.build_flag = obj.build_flag;
|
|
if (obj.building) grid.addBuilding(obj.building);
|
|
}
|
|
});
|
|
}
|
|
if (cfg.map.entrance) { var eg = m.getGrid(cfg.map.entrance[0], cfg.map.entrance[1]); if(eg) { eg.is_entrance = true; eg.passable_flag = 1; eg.build_flag = 0; m.entrance = eg; } }
|
|
if (cfg.map.exit) { var xg = m.getGrid(cfg.map.exit[0], cfg.map.exit[1]); if(xg) { xg.is_exit = true; xg.passable_flag = 1; xg.build_flag = 0; m.exit = xg; } }
|
|
}
|
|
|
|
// 3. Reset Engine States
|
|
TD.stage.current_act.current_scene.wave = 0;
|
|
TD.stage.wait_new_wave = TD.exp_fps * 5; // 5-second start timer
|
|
|
|
document.getElementById("campaign-modal-overlay").style.display = "none";
|
|
TD.stage.current_act.current_scene.start(); // Unpause engine
|
|
});
|
|
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
<!-- ========================================== -->
|
|
<!-- NEW: TOWER TOOLTIP & MODAL SYSTEM -->
|
|
<!-- ========================================== -->
|
|
|
|
<!-- The Hover Tooltip -->
|
|
<div id="tower-tooltip" style="display: none; position: absolute; z-index: 9999; background: rgba(0,20,0,0.9); color: #0f0; padding: 10px; border: 1px solid #0f0; border-radius: 4px; pointer-events: none; font-family: 'Courier New', monospace; box-shadow: 0 0 10px rgba(0,255,0,0.2); font-size: 12px; white-space: pre-line;">
|
|
<!-- Data injected via JS -->
|
|
</div>
|
|
|
|
<!-- NEW: V.A.T.S. Targeting Box -->
|
|
<div id="vats-tooltip" style="display: none; position: absolute; z-index: 10000; width: 60px; height: 45px; background: rgba(0, 40, 0, 0.4); pointer-events: none; border: 1px solid rgba(0, 255, 0, 0.1);">
|
|
<!-- Full Brackets -->
|
|
<div style="position: absolute; top: -2px; bottom: -2px; left: -2px; width: 10px; border-top: 3px solid #0f0; border-bottom: 3px solid #0f0; border-left: 3px solid #0f0;"></div>
|
|
<div style="position: absolute; top: -2px; bottom: -2px; right: -2px; width: 10px; border-top: 3px solid #0f0; border-bottom: 3px solid #0f0; border-right: 3px solid #0f0;"></div>
|
|
|
|
<!-- Health Bar (Top) -->
|
|
<div style="position: absolute; top: 4px; left: 50%; transform: translateX(-50%); width: 80%; height: 10px; border: 1px solid #0f0; background: rgba(0,0,0,0.8);">
|
|
<div id="vats-tooltip-hp" style="width: 100%; height: 100%; background: #0f0; transition: width 0.1s;"></div>
|
|
</div>
|
|
|
|
<!-- Percentage (Bottom) -->
|
|
<div id="vats-tooltip-perc" style="position: absolute; bottom: 4px; width: 100%; text-align: center; color: #0f0; font-family: 'Courier New', monospace; font-weight: bold; font-size: 16px; text-shadow: 1px 1px 2px #000;">
|
|
100%
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- The Click Modal Overlay -->
|
|
<div id="tower-modal-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: transparent; z-index: 99999; justify-content: center; align-items: center;">
|
|
<div id="tower-modal" style="background: #111; color: #0f0; padding: 20px; border: 2px solid #0f0; border-radius: 8px; width: 450px; font-family: 'Courier New', monospace; box-shadow: 0 0 20px rgba(0,255,0,0.4);">
|
|
|
|
<!-- Header -->
|
|
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px dashed #0f0; padding-bottom: 10px; margin-bottom: 15px;">
|
|
<h2 id="modal-tower-name" style="margin: 0; font-size: 1.4em; text-transform: uppercase;">Tower Name</h2>
|
|
<button id="modal-close-btn" style="background: transparent; border: none; color: #0f0; font-size: 1.5em; cursor: pointer;">×</button>
|
|
</div>
|
|
|
|
<!-- Stats & Image -->
|
|
<div style="display: flex; gap: 15px; margin-bottom: 20px;">
|
|
<div style="width: 64px; height: 64px; border: 1px solid #333; background: #000; display: flex; justify-content: center; align-items: center; font-size: 24px;">
|
|
<div id="modal-tower-img" style="color: #fff;">T</div>
|
|
</div>
|
|
<div id="modal-tower-stats" style="font-size: 0.9em; line-height: 1.6; flex: 1;">
|
|
<!-- Stats injected via JS -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Targeting Selection -->
|
|
<div style="margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between;">
|
|
<label for="modal-target-mode" style="font-size: 0.9em; font-weight: bold;">TARGETING:</label>
|
|
<select id="modal-target-mode" style="background: #222; color: #0f0; border: 1px solid #0f0; padding: 5px; font-family: 'Courier New', monospace; flex: 0.8;">
|
|
<option value="closest">Closest</option>
|
|
<option value="leader">Leader</option>
|
|
<option value="strongest">Strongest</option>
|
|
<option value="weakest">Weakest</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- NEW: UPGRADE PATHS (BTD5 Style) -->
|
|
<div style="border-top: 1px dashed #0f0; padding-top: 15px; margin-bottom: 15px;">
|
|
<!-- Path 1 -->
|
|
<div style="display: flex; flex-direction: column; margin-bottom: 10px;">
|
|
<div style="display: flex; justify-content: flex-end; gap: 4px; margin-bottom: 4px;" id="path1-indicators">
|
|
<!-- 4 boxes injected via JS -->
|
|
</div>
|
|
<button id="modal-btn-path1" class="cmd-btn" style="width: 100%; display: flex; flex-direction: column; align-items: flex-start; background-color: #2a7a2a !important; border: 2px solid #0f0 !important; cursor: pointer; padding: 10px; height: auto; text-align: left;">
|
|
<div style="display: flex; justify-content: space-between; width: 100%; font-size: 1.1em; font-weight: bold; margin-bottom: 5px; color: #000 !important; text-shadow: none !important;">
|
|
<span id="path1-name">Upgrade Name</span>
|
|
<span id="path1-cost" style="color: #000 !important; text-shadow: none !important;">0c</span>
|
|
</div>
|
|
<div id="path1-desc" style="font-size: 0.85em; font-weight: bold; white-space: normal; color: #111 !important; text-shadow: none !important;">
|
|
Upgrade description goes here.
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Path 2 -->
|
|
<div style="display: flex; flex-direction: column;">
|
|
<div style="display: flex; justify-content: flex-end; gap: 4px; margin-bottom: 4px;" id="path2-indicators">
|
|
<!-- 4 boxes injected via JS -->
|
|
</div>
|
|
<button id="modal-btn-path2" class="cmd-btn" style="width: 100%; display: flex; flex-direction: column; align-items: flex-start; background-color: #2a7a2a !important; border: 2px solid #0f0 !important; cursor: pointer; padding: 10px; height: auto; text-align: left;">
|
|
<div style="display: flex; justify-content: space-between; width: 100%; font-size: 1.1em; font-weight: bold; margin-bottom: 5px; color: #000 !important; text-shadow: none !important;">
|
|
<span id="path2-name">Upgrade Name</span>
|
|
<span id="path2-cost" style="color: #000 !important; text-shadow: none !important;">0c</span>
|
|
</div>
|
|
<div id="path2-desc" style="font-size: 0.85em; font-weight: bold; white-space: normal; color: #111 !important; text-shadow: none !important;">
|
|
Upgrade description goes here.
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sell Button (Pushed to bottom) -->
|
|
<button id="modal-btn-sell" class="cmd-btn" style="width: 100%; background: #330000; color: #ff3333; border: 1px solid #ff3333; cursor: pointer; padding: 10px;">SELL TOWER (0c)</button>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Campaign Transition Modal -->
|
|
<div id="campaign-modal-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,20,0,0.9); z-index: 99999; justify-content: center; align-items: center; backdrop-filter: blur(4px);">
|
|
<div style="background: #111; color: #0f0; padding: 40px; border: 2px solid #0f0; border-radius: 8px; width: 450px; text-align: center; font-family: 'Courier New', monospace; box-shadow: 0 0 30px rgba(0,255,0,0.5);">
|
|
|
|
<h1 id="campaign-modal-title" style="margin-top: 0; font-size: 2.5em; text-transform: uppercase;">AREA CLEARED</h1>
|
|
<p id="campaign-modal-desc" style="font-size: 1.2em; margin-bottom: 20px;">Threat neutralized. Moving to the next sector.</p>
|
|
|
|
<!-- Hidden by default, shown at the end of a Chapter -->
|
|
<div id="campaign-special-reward" style="display: none; background: #0a220a; padding: 15px; border: 1px dashed #0f0; margin-bottom: 20px;">
|
|
<h3 style="margin-top: 0; color: #fff;">CHAPTER COMPLETE</h3>
|
|
<p style="font-size: 0.9em; margin-bottom: 10px;">Select a permanent S.P.E.C.I.A.L. perk:</p>
|
|
<select id="campaign-special-select" style="background: #222; color: #0f0; border: 1px solid #0f0; padding: 10px; font-size: 1.2em; font-family: 'Courier New', monospace; width: 100%;">
|
|
<option value="S">Strength</option>
|
|
<option value="P">Perception</option>
|
|
<option value="E">Endurance</option>
|
|
<option value="C">Charisma</option>
|
|
<option value="I">Intelligence</option>
|
|
<option value="A">Agility</option>
|
|
<option value="L">Luck</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button id="campaign-btn-proceed" class="cmd-btn sys-btn" style="padding: 15px 30px; background: #2a7a2a; color: #fff; font-size: 1.5em; width: 100%; cursor: pointer;">PROCEED</button>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- NEW: Campaign Story Intro Modal -->
|
|
<div id="campaign-intro-modal-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 100000; justify-content: center; align-items: center; backdrop-filter: blur(5px);">
|
|
<div style="background: #1a1a1a; color: #0f0; padding: 40px; border: 2px solid #0f0; border-radius: 8px; width: 600px; max-width: 90%; text-align: center; font-family: 'Courier New', monospace; box-shadow: 0 0 30px rgba(0,255,0,0.4);">
|
|
|
|
<h1 id="campaign-intro-title" style="margin-top: 0; font-size: 2.2em; text-transform: uppercase; border-bottom: 2px dashed #0f0; padding-bottom: 10px;">INCOMING TRANSMISSION</h1>
|
|
|
|
<p id="campaign-intro-desc" style="font-size: 1.1em; line-height: 1.6; margin-bottom: 30px; text-align: left; color: #ccc;">
|
|
\[Story Text Here\]
|
|
</p>
|
|
|
|
<button id="campaign-btn-deploy" class="cmd-btn sys-btn" style="padding: 15px 30px; background: #2a7a2a; color: #fff; font-size: 1.5em; width: 100%; cursor: pointer; border: 2px solid #0f0 !important;">ACKNOWLEDGE</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- --- NEW: Wasteland Guide Icon & Modal --- -->
|
|
|
|
<div id="guide-modal-overlay">
|
|
<div id="guide-book">
|
|
<span class="close-guide" onclick="document.getElementById('guide-modal-overlay').style.display='none';">×</span>
|
|
|
|
<!-- Left Page: Table of Contents -->
|
|
<div class="guide-page" style="border-right: 1px solid rgba(0,0,0,0.1);">
|
|
<h2>Table of Contents</h2>
|
|
<ul class="guide-toc" style="list-style-type: none; padding-left: 10px;">
|
|
<li>Chapter 1: Not Dying (Probably)</li>
|
|
<li>Chapter 2: Water & Rads</li>
|
|
<li>Chapter 3: Weapon Maintenance</li>
|
|
<li>Chapter 4: The 5-Year Rule</li>
|
|
<li>Chapter 5: Bartering Basics</li>
|
|
<li style="color: #500;">Chapter 6: Mutant Beast Field Guide</li>
|
|
</ul>
|
|
<div style="margin-top: 50px; text-align: center; opacity: 0.6;">
|
|
<img src="assets/images/guide.png" style="width: 120px; filter: grayscale(100%) contrast(200%);" onerror="this.style.display='none'" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Page: Monster Guide -->
|
|
<div class="guide-page">
|
|
<h2>Mutant Field Guide</h2>
|
|
<div id="guide-monster-list">
|
|
<!-- Populated via JS -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NEW: Asset Library Modal Overlay -->
|
|
<div id="asset-modal-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 100000; justify-content: center; align-items: center;">
|
|
<div style="background: #1a1a1a; color: #0f0; padding: 20px; border: 2px solid #0f0; border-radius: 8px; width: 800px; max-width: 95vw; height: 80vh; display: flex; flex-direction: column; font-family: 'Courier New', monospace; box-shadow: 0 0 20px rgba(0,255,0,0.3);">
|
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 2px dashed #0f0; padding-bottom: 10px; margin-bottom: 15px;">
|
|
<h2 style="margin: 0; font-size: 1.5em;">G.E.C.K. ASSET LIBRARY</h2>
|
|
<button onclick="document.getElementById('asset-modal-overlay').style.display='none';" style="background: transparent; border: none; color: #0f0; font-size: 2em; cursor: pointer; line-height: 0.5;">×</button>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 15px; margin-bottom: 15px;">
|
|
<label style="font-weight: bold; align-self: center;">THEME:</label>
|
|
<select id="asset-theme-select" style="background: #222; color: #0f0; border: 1px solid #0f0; padding: 8px; font-family: 'Courier New', monospace; flex: 1;">
|
|
<!-- Populated by JS -->
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Asset Grid Container -->
|
|
<div id="asset-grid-container" style="flex: 1; overflow-y: auto; background: #111; border: 1px solid #333; padding: 10px; display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 15px; align-content: start;">
|
|
<!-- Populated by JS -->
|
|
</div>
|
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- NEW: Floating Admin Cheat Window -->
|
|
<div id="dev-cheat-window" style="display: none; position: absolute; top: 10%; left: 10%; width: 300px; background: rgba(0, 20, 0, 0.85); border: 2px solid #0f0; border-radius: 6px; font-family: 'Courier New', monospace; color: #0f0; z-index: 1000000; box-shadow: 0 0 15px rgba(0,255,0,0.3); backdrop-filter: blur(4px);">
|
|
<!-- Draggable Header -->
|
|
<div id="dev-cheat-header" style="background: rgba(0, 50, 0, 0.9); padding: 8px; cursor: move; border-bottom: 1px solid #0f0; display: flex; justify-content: space-between; align-items: center; border-radius: 4px 4px 0 0;">
|
|
<b style="font-size: 1.1em; letter-spacing: 1px;">OVERSEER CONSOLE</b>
|
|
<button onclick="document.getElementById('dev-cheat-window').style.display='none';" style="background: transparent; border: none; color: #0f0; font-size: 1.5em; cursor: pointer; line-height: 0.5;">×</button>
|
|
</div>
|
|
|
|
<!-- Tool Container -->
|
|
<div style="padding: 15px; display: flex; flex-direction: column; gap: 10px; font-size: 0.9em;">
|
|
|
|
<!-- Raw Stats -->
|
|
<div style="display: flex; gap: 5px; align-items: center;">
|
|
<label style="width: 60px;">CAPS:</label>
|
|
<input type="number" id="dev-input-caps" value="1000" style="flex: 1; background: #111; color: #0f0; border: 1px solid #0f0; padding: 3px;">
|
|
<button id="dev-btn-caps" class="cmd-btn" style="background: #050; border-color: #0f0; padding: 3px 10px;">GIVE</button>
|
|
</div>
|
|
<div style="display: flex; gap: 5px; align-items: center;">
|
|
<label style="width: 60px;">SCORE:</label>
|
|
<input type="number" id="dev-input-score" value="5000" style="flex: 1; background: #111; color: #0f0; border: 1px solid #0f0; padding: 3px;">
|
|
<button id="dev-btn-score" class="cmd-btn" style="background: #050; border-color: #0f0; padding: 3px 10px;">GIVE</button>
|
|
</div>
|
|
<div style="display: flex; gap: 5px; align-items: center;">
|
|
<label style="width: 60px;">HEALTH:</label>
|
|
<input type="number" id="dev-input-health" value="100" style="flex: 1; background: #111; color: #0f0; border: 1px solid #0f0; padding: 3px;">
|
|
<button id="dev-btn-health" class="cmd-btn" style="background: #050; border-color: #0f0; padding: 3px 10px;">SET</button>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 5px; align-items: center;">
|
|
<label style="width: 60px;">VATS %:</label>
|
|
<input type="number" id="dev-input-vats" value="100" min="0" max="100" style="width: 90px; flex-shrink: 0; background: #111; color: #0f0; border: 1px solid #0f0; padding: 3px;">
|
|
<button id="dev-btn-vats" class="cmd-btn" style="flex: 1; background: #050; border-color: #0f0; padding: 3px 10px;">SET</button>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 5px; align-items: center;">
|
|
<label style="width: 60px;">WAVE:</label>
|
|
<input type="number" id="dev-input-wave" value="15" min="1" style="width: 90px; flex-shrink: 0; background: #111; color: #0f0; border: 1px solid #0f0; padding: 3px;">
|
|
<button id="dev-btn-wave" class="cmd-btn" style="flex: 1; background: #050; border-color: #0f0; padding: 3px 10px;">JUMP</button>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 5px; align-items: center;">
|
|
<label style="width: 60px;">SPAWN:</label>
|
|
<select id="dev-sel-spawn" style="width: 90px; flex-shrink: 0; background: #111; color: #0f0; border: 1px solid #0f0; padding: 3px;">
|
|
<option value="0">Radroach</option>
|
|
<option value="1">Feral Ghoul</option>
|
|
<option value="2">Mole Rat</option>
|
|
<option value="3">Super Mutant</option>
|
|
<option value="4">Protectron</option>
|
|
<option value="5">Glowing One</option>
|
|
<option value="6">Deathclaw</option>
|
|
<option value="7">Sentry Bot</option>
|
|
<option value="8">Cazador (Fly)</option>
|
|
<option value="9">Stingwing (Fly)</option>
|
|
<option value="10">Scorchbeast (Fly)</option>
|
|
<option value="11">Nightkin (Stealth)</option>
|
|
</select>
|
|
<button id="dev-btn-spawn" class="cmd-btn" style="flex: 1; background: #050; border-color: #0f0; padding: 3px 10px;">DROP</button>
|
|
</div>
|
|
|
|
<hr style="border-color: #050; width: 100%; margin: 5px 0;">
|
|
|
|
<!-- Unlock Towers -->
|
|
<div style="display: flex; gap: 5px; align-items: center;">
|
|
<label style="width: 60px;">UNLOCK:</label>
|
|
<select id="dev-sel-tower" style="width: 90px; flex-shrink: 0; background: #111; color: #0f0; border: 1px solid #0f0; padding: 3px;">
|
|
<option value="all">ALL TOWERS</option>
|
|
<option value="t5">Tower 5</option>
|
|
<option value="t6">Tower 6</option>
|
|
<option value="t7">Tower 7</option>
|
|
<option value="t8">Tower 8</option>
|
|
<option value="t9">Tower 9</option>
|
|
<option value="t10">Tower 10</option>
|
|
</select>
|
|
<button id="dev-btn-tower" class="cmd-btn" style="flex: 1; background: #050; border-color: #0f0; padding: 3px 10px;">DO IT</button>
|
|
</div>
|
|
|
|
<!-- Items -->
|
|
<div style="display: flex; gap: 5px; align-items: center;">
|
|
<label style="width: 60px;">ITEM:</label>
|
|
<select id="dev-sel-item" style="width: 90px; flex-shrink: 0; background: #111; color: #0f0; border: 1px solid #0f0; padding: 3px;">
|
|
<option value="stimpak">Stimpak</option>
|
|
<option value="psycho">Psycho</option>
|
|
<option value="cryomine">Cryo Mine</option>
|
|
<option value="capmine">Cap Mine</option>
|
|
<option value="artillery">Artillery</option>
|
|
</select>
|
|
<input type="number" id="dev-input-item" value="1" min="1" max="10" style="width: 30px; flex-shrink: 0; background: #111; color: #0f0; border: 1px solid #0f0; padding: 3px; text-align: center;">
|
|
<button id="dev-btn-item" class="cmd-btn" style="flex: 1; background: #050; border-color: #0f0; padding: 3px 10px;">ADD</button>
|
|
</div>
|
|
|
|
<!-- Forced Drop -->
|
|
<div style="display: flex; gap: 5px; align-items: center;">
|
|
<label style="width: 60px; font-size: 0.8em;">FORCE DROP:</label>
|
|
<select id="dev-sel-drop" style="width: 90px; flex-shrink: 0; background: #111; color: #0f0; border: 1px solid #0f0; padding: 3px;">
|
|
<option value="none">Normal RNG</option>
|
|
<option value="money">Caps</option>
|
|
<option value="lunchbox">Lunchbox</option>
|
|
<option value="stimpak">Stimpak</option>
|
|
<option value="psycho">Psycho</option>
|
|
<option value="cryomine">Cryo Mine</option>
|
|
<option value="capmine">Cap Mine</option>
|
|
<option value="faction">Faction Ult</option>
|
|
</select>
|
|
<button id="dev-btn-drop" class="cmd-btn" style="flex: 1; background: #050; border-color: #0f0; padding: 3px 10px;">SET</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- NEW: Custom Dropdown Logic -->
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
var selects = document.querySelectorAll("#start-screen select.menu-select");
|
|
selects.forEach(function(select) {
|
|
// Hide original select
|
|
select.style.display = "none";
|
|
|
|
// Create wrapper
|
|
var wrapper = document.createElement("div");
|
|
wrapper.className = "custom-select-wrapper";
|
|
select.parentNode.insertBefore(wrapper, select);
|
|
wrapper.appendChild(select);
|
|
|
|
// Create the visible 'closed' display
|
|
var display = document.createElement("div");
|
|
display.className = "menu-select";
|
|
display.style.marginBottom = "0";
|
|
display.style.width = "100%";
|
|
display.style.maxWidth = "100%";
|
|
|
|
// Override CSS to force a perfect flexbox center
|
|
display.style.display = "flex";
|
|
display.style.alignItems = "center";
|
|
display.style.justifyContent = "center";
|
|
display.style.padding = "10px 35px";
|
|
|
|
// Wrap the text in a span to protect it from line-break rendering bugs
|
|
var textSpan = document.createElement("span");
|
|
textSpan.style.textAlign = "center";
|
|
textSpan.style.lineHeight = "1.2";
|
|
textSpan.innerHTML = select.options[select.selectedIndex].innerHTML;
|
|
display.appendChild(textSpan);
|
|
|
|
// Transfer tooltip hover effects from original select
|
|
|
|
display.onmouseover = select.onmouseover;
|
|
display.onmouseout = select.onmouseout;
|
|
|
|
wrapper.appendChild(display);
|
|
|
|
// Create the dropdown options container
|
|
var optionsContainer = document.createElement("div");
|
|
optionsContainer.className = "custom-select-options";
|
|
wrapper.appendChild(optionsContainer);
|
|
|
|
// Populate options
|
|
Array.from(select.options).forEach(function(option) {
|
|
var optDiv = document.createElement("div");
|
|
optDiv.className = "custom-option";
|
|
optDiv.innerHTML = option.innerHTML;
|
|
|
|
// NEW: Mark the initially selected option so the arrow appears on load
|
|
if (option.selected) {
|
|
optDiv.classList.add("selected");
|
|
}
|
|
|
|
// Handle selecting an option
|
|
optDiv.addEventListener("click", function(e) {
|
|
e.stopPropagation();
|
|
textSpan.innerHTML = this.innerHTML; // Update the span, not the div
|
|
select.value = option.value;
|
|
|
|
// NEW: Remove the arrow from all options, then add it to the clicked one
|
|
|
|
Array.from(optionsContainer.children).forEach(function(sibling) {
|
|
sibling.classList.remove("selected");
|
|
});
|
|
this.classList.add("selected");
|
|
|
|
optionsContainer.classList.remove("open");
|
|
|
|
// Trigger the change event on the original select so game logic updates
|
|
var event = new Event('change');
|
|
select.dispatchEvent(event);
|
|
});
|
|
optionsContainer.appendChild(optDiv);
|
|
});
|
|
|
|
|
|
// Toggle dropdown open/close
|
|
display.addEventListener("click", function(e) {
|
|
e.stopPropagation();
|
|
// Close other open dropdowns
|
|
document.querySelectorAll(".custom-select-options").forEach(function(opt) {
|
|
if (opt !== optionsContainer) opt.classList.remove("open");
|
|
});
|
|
optionsContainer.classList.toggle("open");
|
|
});
|
|
});
|
|
|
|
// Close dropdowns when clicking outside
|
|
document.addEventListener("click", function() {
|
|
document.querySelectorAll(".custom-select-options").forEach(function(opt) {
|
|
opt.classList.remove("open");
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<!-- Load External Map Data -->
|
|
<script src="assets/maps.js"></script>
|
|
|
|
|
|
</body>
|
|
</html>
|
|
|
|
|