helldivers-theme 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +50 -0
- data/_config.yml +40 -0
- data/_data/squad.yaml +127 -0
- data/_layouts/default.html +61 -0
- data/_sass/_base.scss +88 -0
- data/_sass/_components.scss +1100 -0
- data/_sass/_layout.scss +205 -0
- data/_sass/_sidebar.scss +49 -0
- data/_sass/_variables.scss +19 -0
- data/about.md +107 -0
- data/assets/css/style.scss +8 -0
- data/assets/img/bg/space-deck.jpg +0 -0
- data/assets/js/main.js +57 -0
- data/assets/js/particles.js +119 -0
- data/assets/js/profile-modal.js +110 -0
- data/assets/main.scss +7 -0
- data/contact.md +55 -0
- data/index.html +66 -0
- data/join.md +75 -0
- data/watch.md +51 -0
- metadata +119 -0
data/_sass/_layout.scss
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// _sass/_layout.scss
|
|
2
|
+
|
|
3
|
+
.terminal-wrapper {
|
|
4
|
+
width: 100%;
|
|
5
|
+
max-width: 1200px;
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: 25px;
|
|
9
|
+
position: relative;
|
|
10
|
+
z-index: 10;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.terminal-header {
|
|
14
|
+
display: flex;
|
|
15
|
+
justify-content: space-between;
|
|
16
|
+
align-items: center;
|
|
17
|
+
padding: 20px 30px;
|
|
18
|
+
|
|
19
|
+
// Glass Header
|
|
20
|
+
background: rgba(2, 4, 10, 0.8);
|
|
21
|
+
backdrop-filter: blur(10px);
|
|
22
|
+
border-bottom: 2px solid $hd-gold;
|
|
23
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
|
24
|
+
|
|
25
|
+
// Angled corners for sci-fi look
|
|
26
|
+
clip-path: polygon(0 0, 100% 0, 100% 75%, 98% 100%, 2% 100%, 0 75%);
|
|
27
|
+
|
|
28
|
+
h1 {
|
|
29
|
+
font-family: $font-head;
|
|
30
|
+
font-size: 3.5rem;
|
|
31
|
+
color: $hd-white;
|
|
32
|
+
text-transform: uppercase;
|
|
33
|
+
margin: 0;
|
|
34
|
+
text-shadow: 0 0 10px rgba($hd-gold, 0.6);
|
|
35
|
+
letter-spacing: 2px;
|
|
36
|
+
|
|
37
|
+
span {
|
|
38
|
+
color: $hd-gold;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.subtitle {
|
|
43
|
+
color: $hd-cyan;
|
|
44
|
+
font-size: 0.9rem;
|
|
45
|
+
letter-spacing: 3px;
|
|
46
|
+
text-shadow: 0 0 5px $hd-cyan;
|
|
47
|
+
display: block;
|
|
48
|
+
margin-top: 5px;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.main-nav {
|
|
53
|
+
display: flex;
|
|
54
|
+
gap: 25px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.nav-link {
|
|
58
|
+
text-decoration: none;
|
|
59
|
+
color: $hd-white;
|
|
60
|
+
font-family: $font-head;
|
|
61
|
+
font-size: 1.4rem;
|
|
62
|
+
letter-spacing: 2px;
|
|
63
|
+
transition: all 0.3s ease;
|
|
64
|
+
text-transform: uppercase;
|
|
65
|
+
position: relative;
|
|
66
|
+
|
|
67
|
+
&:visited { color: $hd-white; }
|
|
68
|
+
|
|
69
|
+
&:hover {
|
|
70
|
+
color: $hd-cyan;
|
|
71
|
+
text-shadow: 0 0 12px $hd-cyan;
|
|
72
|
+
transform: translateY(-2px);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
&.active {
|
|
76
|
+
color: $hd-gold;
|
|
77
|
+
text-shadow: 0 0 15px $hd-gold;
|
|
78
|
+
|
|
79
|
+
// Little underline indicator
|
|
80
|
+
&::after {
|
|
81
|
+
content: '';
|
|
82
|
+
position: absolute;
|
|
83
|
+
bottom: -5px;
|
|
84
|
+
left: 0;
|
|
85
|
+
width: 100%;
|
|
86
|
+
height: 2px;
|
|
87
|
+
background: $hd-gold;
|
|
88
|
+
box-shadow: 0 0 10px $hd-gold;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Existing Red Pulse
|
|
94
|
+
.live-dot {
|
|
95
|
+
display: inline-block;
|
|
96
|
+
width: 8px;
|
|
97
|
+
height: 8px;
|
|
98
|
+
background-color: $hd-red; // Default to Red (Offline)
|
|
99
|
+
border-radius: 50%;
|
|
100
|
+
margin-left: 6px;
|
|
101
|
+
box-shadow: 0 0 5px $hd-red;
|
|
102
|
+
animation: pulse-red 1.5s infinite;
|
|
103
|
+
transition: all 0.5s ease; // Smooth color change
|
|
104
|
+
flex-shrink: 0; // CRITICAL: Prevents the dot from squishing
|
|
105
|
+
margin: 0; // Remove default margins, use gap in parent instead
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// New Green Pulse (Added via JS when live)
|
|
109
|
+
.live-dot.is-live {
|
|
110
|
+
background-color: #0f0; // Bright Green
|
|
111
|
+
box-shadow: 0 0 8px #0f0;
|
|
112
|
+
animation: pulse-green 1.5s infinite;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@keyframes pulse-red {
|
|
116
|
+
0% { opacity: 1; transform: scale(1); }
|
|
117
|
+
50% { opacity: 0.5; transform: scale(1.2); }
|
|
118
|
+
100% { opacity: 1; transform: scale(1); }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@keyframes pulse-green {
|
|
122
|
+
0% { opacity: 1; transform: scale(1); box-shadow: 0 0 5px #0f0; }
|
|
123
|
+
50% { opacity: 0.6; transform: scale(1.3); box-shadow: 0 0 15px #0f0; }
|
|
124
|
+
100% { opacity: 1; transform: scale(1); box-shadow: 0 0 5px #0f0; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// THE MAIN HOLOGRAPHIC CARD
|
|
128
|
+
|
|
129
|
+
// _sass/_layout.scss
|
|
130
|
+
|
|
131
|
+
.terminal-card {
|
|
132
|
+
display: grid;
|
|
133
|
+
// DEFAULT: Sidebar (280px) + Content (Rest)
|
|
134
|
+
// This applies to the Home Page automatically because it has an <aside>
|
|
135
|
+
grid-template-columns: 280px 1fr;
|
|
136
|
+
gap: 25px;
|
|
137
|
+
|
|
138
|
+
// Glassmorphism & Borders
|
|
139
|
+
background: $glass-bg;
|
|
140
|
+
backdrop-filter: blur(12px);
|
|
141
|
+
-webkit-backdrop-filter: blur(12px);
|
|
142
|
+
border: 1px solid $glass-border;
|
|
143
|
+
border-top: 1px solid rgba($hd-gold, 0.6);
|
|
144
|
+
border-left: 1px solid rgba($hd-cyan, 0.3);
|
|
145
|
+
padding: 30px;
|
|
146
|
+
border-radius: 4px;
|
|
147
|
+
box-shadow: 0 20px 50px rgba(0,0,0,0.6), inset 0 0 30px rgba(0, 240, 255, 0.05);
|
|
148
|
+
position: relative;
|
|
149
|
+
min-height: 600px;
|
|
150
|
+
|
|
151
|
+
// Decorative Corners
|
|
152
|
+
&::before, &::after {
|
|
153
|
+
content: '';
|
|
154
|
+
position: absolute;
|
|
155
|
+
width: 20px;
|
|
156
|
+
height: 20px;
|
|
157
|
+
border: 2px solid $hd-cyan;
|
|
158
|
+
transition: all 0.3s ease;
|
|
159
|
+
z-index: 10;
|
|
160
|
+
}
|
|
161
|
+
&::before { top: -2px; left: -2px; border-right: none; border-bottom: none; }
|
|
162
|
+
&::after { bottom: -2px; right: -2px; border-left: none; border-top: none; }
|
|
163
|
+
|
|
164
|
+
&:hover {
|
|
165
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.7), inset 0 0 40px rgba(0, 240, 255, 0.1);
|
|
166
|
+
&::before, &::after {
|
|
167
|
+
width: 30px;
|
|
168
|
+
height: 30px;
|
|
169
|
+
border-color: $hd-gold;
|
|
170
|
+
box-shadow: 0 0 10px $hd-gold;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- EXCEPTION: ENLIST & INTEL PAGES ---
|
|
175
|
+
// If the direct child is our specific centered containers, force single column
|
|
176
|
+
&:has(> .enlist-container),
|
|
177
|
+
&:has(> .intel-container),
|
|
178
|
+
&:has(> .comms-container),
|
|
179
|
+
&:has(> .watch-container) {
|
|
180
|
+
grid-template-columns: 1fr; // Remove sidebar column
|
|
181
|
+
justify-items: center; // Center content
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Force these containers to span full width and center themselves
|
|
186
|
+
.enlist-container, .intel-container, .comms-container, .watch-container {
|
|
187
|
+
grid-column: 1 / -1; // Span across all columns
|
|
188
|
+
width: 100%;
|
|
189
|
+
max-width: 1200px; // Cap width so it doesn't stretch too far
|
|
190
|
+
margin: 0 auto; // Center horizontally
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
align-items: center;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Mobile Responsiveness
|
|
197
|
+
@media (max-width: 900px) {
|
|
198
|
+
.terminal-card {
|
|
199
|
+
grid-template-columns: 1fr; // Stack sidebar on top for mobile
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.enlist-container, .intel-container {
|
|
203
|
+
max-width: 100%;
|
|
204
|
+
}
|
|
205
|
+
}
|
data/_sass/_sidebar.scss
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// _sass/_sidebar.scss
|
|
2
|
+
|
|
3
|
+
.sidebar {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: 20px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.panel-box {
|
|
10
|
+
background: rgba(0, 0, 0, 0.4);
|
|
11
|
+
border: 1px solid rgba($hd-cyan, 0.2);
|
|
12
|
+
padding: 15px;
|
|
13
|
+
position: relative;
|
|
14
|
+
backdrop-filter: blur(5px);
|
|
15
|
+
|
|
16
|
+
// Small corner accents
|
|
17
|
+
&::before {
|
|
18
|
+
content: '';
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: 0; left: 0;
|
|
21
|
+
width: 10px; height: 10px;
|
|
22
|
+
border-top: 2px solid $hd-gold;
|
|
23
|
+
border-left: 2px solid $hd-gold;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
h3 {
|
|
27
|
+
font-family: $font-head;
|
|
28
|
+
color: $hd-cyan;
|
|
29
|
+
font-size: 1.2rem;
|
|
30
|
+
margin-bottom: 10px;
|
|
31
|
+
border-bottom: 1px solid rgba($hd-cyan, 0.3);
|
|
32
|
+
padding-bottom: 5px;
|
|
33
|
+
text-transform: uppercase;
|
|
34
|
+
letter-spacing: 1px;
|
|
35
|
+
text-shadow: 0 0 5px rgba($hd-cyan, 0.4);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.alert-marquee {
|
|
40
|
+
background: rgba($hd-red, 0.2); // Transparent red
|
|
41
|
+
border: 1px solid $hd-red;
|
|
42
|
+
color: #ffaaaa;
|
|
43
|
+
// ... keep animation code same ...
|
|
44
|
+
|
|
45
|
+
.marquee-content span::before {
|
|
46
|
+
color: $hd-red;
|
|
47
|
+
text-shadow: 0 0 5px $hd-red;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// _sass/_variables.scss
|
|
2
|
+
|
|
3
|
+
// Core Colors
|
|
4
|
+
$hd-blue-deep: #02040a; // Almost black space
|
|
5
|
+
$hd-blue-space: #0b1021; // Deep navy
|
|
6
|
+
$hd-gold: #ffd700; // Liberty Gold
|
|
7
|
+
$hd-red: #ff2a2a; // Alert Crimson
|
|
8
|
+
$hd-cyan: #00f0ff; // Holographic Tech Blue
|
|
9
|
+
$hd-white: #e6f2ff; // Slightly blue-tinted white
|
|
10
|
+
|
|
11
|
+
// Fonts
|
|
12
|
+
$font-head: 'Bebas Neue', sans-serif;
|
|
13
|
+
$font-mono: 'Share Tech Mono', monospace;
|
|
14
|
+
|
|
15
|
+
// Effects
|
|
16
|
+
$glass-bg: rgba(11, 16, 33, 0.65); // Transparent dark blue
|
|
17
|
+
$glass-border: rgba(255, 215, 0, 0.3); // Faint gold border
|
|
18
|
+
$glow-gold: 0 0 15px rgba(255, 215, 0, 0.4);
|
|
19
|
+
$glow-cyan: 0 0 15px rgba(0, 240, 255, 0.4);
|
data/about.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Intel
|
|
4
|
+
permalink: /about/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<div class="intel-container">
|
|
8
|
+
|
|
9
|
+
<!-- HEADER & QUOTE -->
|
|
10
|
+
<div class="section-header">
|
|
11
|
+
<h2>// SQUAD DOSSIER: {{ site.data.squad.squad_name | upcase }}</h2>
|
|
12
|
+
<div class="line-accent"></div>
|
|
13
|
+
<p class="squad-motto" style="margin-top: 15px; color: var(--hd-cyan); font-style: italic; font-size: 1.1rem;">
|
|
14
|
+
{{ site.data.squad.motto }} <span style="color: #888;">{{ site.data.squad.commander_quote }}</span>
|
|
15
|
+
</p>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- SMALL GAP AFTER QUOTE -->
|
|
19
|
+
<div class="intel-spacer"></div>
|
|
20
|
+
|
|
21
|
+
<!-- TOP ROW: STATS (Left) + LOADOUTS (Right) -->
|
|
22
|
+
<div class="intel-dashboard-row">
|
|
23
|
+
|
|
24
|
+
<!-- STATS PANEL (Left Side) -->
|
|
25
|
+
<div class="stats-panel-compact">
|
|
26
|
+
<h3>// OPERATIONAL STATS</h3>
|
|
27
|
+
<ul class="stat-list-compact">
|
|
28
|
+
{% for stat in site.data.squad.stats %}
|
|
29
|
+
<li>
|
|
30
|
+
<span class="stat-label">{{ stat.label }}:</span>
|
|
31
|
+
<span class="stat-value" style="{% if stat.color %}color: {{ stat.color }};{% endif %}">
|
|
32
|
+
{{ stat.value }}
|
|
33
|
+
</span>
|
|
34
|
+
</li>
|
|
35
|
+
{% endfor %}
|
|
36
|
+
</ul>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<!-- LOADOUT TABS (Right Side - Takes remaining space) -->
|
|
40
|
+
<div class="panel-box intel-panel loadout-panel-compact">
|
|
41
|
+
<h3>// TACTICAL LOADOUT DATABASE</h3>
|
|
42
|
+
|
|
43
|
+
<!-- Tab Buttons -->
|
|
44
|
+
<div class="loadout-tabs">
|
|
45
|
+
{% for loadout in site.data.squad.loadouts %}
|
|
46
|
+
<button class="tab-btn {% if forloop.first %}active{% endif %}"
|
|
47
|
+
onclick="openLoadout(event, 'loadout-{{ forloop.index }}')"
|
|
48
|
+
data-color="{{ loadout.theme_color }}">
|
|
49
|
+
{{ loadout.name }}
|
|
50
|
+
</button>
|
|
51
|
+
{% endfor %}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<!-- Tab Content Areas -->
|
|
55
|
+
<div class="loadout-content-area">
|
|
56
|
+
{% for loadout in site.data.squad.loadouts %}
|
|
57
|
+
<div id="loadout-{{ forloop.index }}"
|
|
58
|
+
class="loadout-tab-content {% if forloop.first %}active{% endif %}"
|
|
59
|
+
data-theme="{{ loadout.theme_color }}">
|
|
60
|
+
|
|
61
|
+
<div class="loadout-header" style="border-bottom-color: var(--hd-{{ loadout.theme_color }}); color: var(--hd-{{ loadout.theme_color }});">
|
|
62
|
+
{{ loadout.name | upcase }}
|
|
63
|
+
</div>
|
|
64
|
+
<ul class="loadout-items">
|
|
65
|
+
{% for item in loadout.items %}
|
|
66
|
+
<li>{{ item }}</li>
|
|
67
|
+
{% endfor %}
|
|
68
|
+
</ul>
|
|
69
|
+
</div>
|
|
70
|
+
{% endfor %}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- LARGE GAP BEFORE STORY -->
|
|
76
|
+
<div class="intel-spacer-large"></div>
|
|
77
|
+
|
|
78
|
+
<!-- BOTTOM ROW: ORIGIN STORY (Full Width) -->
|
|
79
|
+
<div class="panel-box intel-panel story-panel-wide">
|
|
80
|
+
<h3>// ORIGIN STORY</h3>
|
|
81
|
+
<div class="story-content">
|
|
82
|
+
{{ site.data.squad.story }}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<!-- JAVASCRIPT FOR TABS -->
|
|
89
|
+
<script>
|
|
90
|
+
function openLoadout(evt, loadoutName) {
|
|
91
|
+
var i, tabcontent, tablinks;
|
|
92
|
+
tabcontent = document.getElementsByClassName("loadout-tab-content");
|
|
93
|
+
for (i = 0; i < tabcontent.length; i++) {
|
|
94
|
+
tabcontent[i].style.display = "none";
|
|
95
|
+
tabcontent[i].classList.remove("active");
|
|
96
|
+
}
|
|
97
|
+
tablinks = document.getElementsByClassName("tab-btn");
|
|
98
|
+
for (i = 0; i < tablinks.length; i++) {
|
|
99
|
+
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
|
100
|
+
}
|
|
101
|
+
document.getElementById(loadoutName).style.display = "block";
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
document.getElementById(loadoutName).classList.add("active");
|
|
104
|
+
}, 10);
|
|
105
|
+
evt.currentTarget.className += " active";
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
Binary file
|
data/assets/js/main.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
+
|
|
3
|
+
// --- 1. CONFIGURATION ---
|
|
4
|
+
// Official Helldivers 2 Steam News Feed URL
|
|
5
|
+
const RSS_URL = 'https://store.steampowered.com/feeds/news/app/553850/';
|
|
6
|
+
const FEED_CONTAINER = document.getElementById('rss-feed');
|
|
7
|
+
|
|
8
|
+
// --- 2. FETCH UPDATES FUNCTION ---
|
|
9
|
+
async function loadGameUpdates() {
|
|
10
|
+
try {
|
|
11
|
+
// We use rss2json.com to convert the XML feed to JSON and bypass CORS
|
|
12
|
+
const apiUrl = `https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent(RSS_URL)}`;
|
|
13
|
+
|
|
14
|
+
const response = await fetch(apiUrl);
|
|
15
|
+
|
|
16
|
+
if (!response.ok) throw new Error('Network response was not ok');
|
|
17
|
+
|
|
18
|
+
const data = await response.json();
|
|
19
|
+
|
|
20
|
+
// Clear the loading text
|
|
21
|
+
FEED_CONTAINER.innerHTML = '';
|
|
22
|
+
|
|
23
|
+
// Create a list for the items
|
|
24
|
+
const list = document.createElement('ul');
|
|
25
|
+
list.className = 'news-list';
|
|
26
|
+
|
|
27
|
+
// Loop through the top 5 news items
|
|
28
|
+
data.items.slice(0, 5).forEach(item => {
|
|
29
|
+
const li = document.createElement('li');
|
|
30
|
+
|
|
31
|
+
// Format the date nicely
|
|
32
|
+
const dateObj = new Date(item.pubDate);
|
|
33
|
+
const dateStr = dateObj.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
34
|
+
|
|
35
|
+
li.innerHTML = `
|
|
36
|
+
<span class="news-date">[${dateStr}]</span>
|
|
37
|
+
<a href="${item.link}" target="_blank" class="news-link">${item.title}</a>
|
|
38
|
+
`;
|
|
39
|
+
list.appendChild(li);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
FEED_CONTAINER.appendChild(list);
|
|
43
|
+
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Failed to load updates:', error);
|
|
46
|
+
FEED_CONTAINER.innerHTML = `
|
|
47
|
+
<div class="error-msg">
|
|
48
|
+
⚠️ COMM LINK INTERRUPTED<br>
|
|
49
|
+
<small>Unable to reach Super Earth servers.</small>
|
|
50
|
+
</div>
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- 3. INITIALIZE ---
|
|
56
|
+
loadGameUpdates();
|
|
57
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// assets/js/bg-particles.js
|
|
2
|
+
|
|
3
|
+
const canvas = document.createElement('canvas');
|
|
4
|
+
const ctx = canvas.getContext('2d');
|
|
5
|
+
document.body.appendChild(canvas);
|
|
6
|
+
|
|
7
|
+
// Style the canvas to sit behind everything
|
|
8
|
+
canvas.style.position = 'fixed';
|
|
9
|
+
canvas.style.top = '0';
|
|
10
|
+
canvas.style.left = '0';
|
|
11
|
+
canvas.style.width = '100%';
|
|
12
|
+
canvas.style.height = '100%';
|
|
13
|
+
canvas.style.zIndex = '0'; // Behind the content
|
|
14
|
+
canvas.style.pointerEvents = 'none'; // Click through it
|
|
15
|
+
canvas.style.opacity = '0.4'; // Subtle effect
|
|
16
|
+
|
|
17
|
+
let width, height;
|
|
18
|
+
let particles = [];
|
|
19
|
+
|
|
20
|
+
// Configuration
|
|
21
|
+
const particleCount = 150; // Fewer particles needed for Canvas to look full
|
|
22
|
+
const connectionDistance = 100;
|
|
23
|
+
const baseHue = 50; // Cyan/Blue (Helldivers Tech vibe)
|
|
24
|
+
|
|
25
|
+
// Resize handling
|
|
26
|
+
function resize() {
|
|
27
|
+
width = window.innerWidth;
|
|
28
|
+
height = window.innerHeight;
|
|
29
|
+
canvas.width = width;
|
|
30
|
+
canvas.height = height;
|
|
31
|
+
}
|
|
32
|
+
window.addEventListener('resize', resize);
|
|
33
|
+
resize();
|
|
34
|
+
|
|
35
|
+
// Particle Class
|
|
36
|
+
class Particle {
|
|
37
|
+
constructor() {
|
|
38
|
+
this.reset();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
reset() {
|
|
42
|
+
this.x = Math.random() * width;
|
|
43
|
+
this.y = Math.random() * height;
|
|
44
|
+
this.vx = (Math.random() - 0.5) * 0.5; // Slow drift velocity
|
|
45
|
+
this.vy = (Math.random() - 0.5) * 0.5;
|
|
46
|
+
this.size = Math.random() * 2 + 1;
|
|
47
|
+
// Vary hue slightly for depth
|
|
48
|
+
this.hue = baseHue + (Math.random() * 40 - 20);
|
|
49
|
+
this.life = Math.random() * 100;
|
|
50
|
+
this.maxLife = 100 + Math.random() * 100;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
update() {
|
|
54
|
+
this.x += this.vx;
|
|
55
|
+
this.y += this.vy;
|
|
56
|
+
this.life++;
|
|
57
|
+
|
|
58
|
+
// Wrap around screen
|
|
59
|
+
if (this.x < 0) this.x = width;
|
|
60
|
+
if (this.x > width) this.x = 0;
|
|
61
|
+
if (this.y < 0) this.y = height;
|
|
62
|
+
if (this.y > height) this.y = 0;
|
|
63
|
+
|
|
64
|
+
// Fade in/out cycle
|
|
65
|
+
if (this.life > this.maxLife) {
|
|
66
|
+
this.reset();
|
|
67
|
+
this.life = 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
draw() {
|
|
72
|
+
const opacity = Math.sin((this.life / this.maxLife) * Math.PI); // Smooth fade
|
|
73
|
+
ctx.fillStyle = `hsla(${this.hue}, 100%, 60%, ${opacity})`;
|
|
74
|
+
ctx.beginPath();
|
|
75
|
+
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
|
76
|
+
ctx.fill();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Initialize
|
|
81
|
+
for (let i = 0; i < particleCount; i++) {
|
|
82
|
+
particles.push(new Particle());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Animation Loop
|
|
86
|
+
function animate() {
|
|
87
|
+
ctx.clearRect(0, 0, width, height);
|
|
88
|
+
|
|
89
|
+
// Draw particles
|
|
90
|
+
particles.forEach(p => {
|
|
91
|
+
p.update();
|
|
92
|
+
p.draw();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Optional: Draw subtle connections (lines) between close particles
|
|
96
|
+
// Only do this if performance allows (comment out if too slow)
|
|
97
|
+
/*
|
|
98
|
+
ctx.strokeStyle = `hsla(${baseHue}, 100%, 50%, 0.1)`;
|
|
99
|
+
ctx.lineWidth = 0.5;
|
|
100
|
+
for (let i = 0; i < particles.length; i++) {
|
|
101
|
+
for (let j = i + 1; j < particles.length; j++) {
|
|
102
|
+
const dx = particles[i].x - particles[j].x;
|
|
103
|
+
const dy = particles[i].y - particles[j].y;
|
|
104
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
105
|
+
|
|
106
|
+
if (dist < connectionDistance) {
|
|
107
|
+
ctx.beginPath();
|
|
108
|
+
ctx.moveTo(particles[i].x, particles[i].y);
|
|
109
|
+
ctx.lineTo(particles[j].x, particles[j].y);
|
|
110
|
+
ctx.stroke();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
requestAnimationFrame(animate);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
animate();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// assets/js/profile-modal.js
|
|
2
|
+
|
|
3
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
4
|
+
const dataElement = document.getElementById('leader-data-json');
|
|
5
|
+
|
|
6
|
+
if (!dataElement) {
|
|
7
|
+
console.error("❌ Error: Hidden data div #leader-data-json not found!");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let squadData = [];
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// Parse the JSON from the hidden div
|
|
15
|
+
squadData = JSON.parse(dataElement.textContent.trim());
|
|
16
|
+
console.log("✅ Leader data loaded:", squadData);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error("❌ CRITICAL ERROR: Failed to parse leader data.");
|
|
19
|
+
console.error("Raw content causing error:", dataElement.textContent);
|
|
20
|
+
console.error("Error details:", error);
|
|
21
|
+
alert("Error loading profile data. Check browser console (F12).");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Define Open Function
|
|
26
|
+
window.openLeaderProfile = function(name) {
|
|
27
|
+
console.log("🔍 Searching for:", name);
|
|
28
|
+
const data = squadData.find(l => l.name === name);
|
|
29
|
+
|
|
30
|
+
if (!data) {
|
|
31
|
+
console.error("❌ Leader not found in data:", name);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log("✅ Found data:", data);
|
|
36
|
+
|
|
37
|
+
// --- 1. Populate Basic Fields ---
|
|
38
|
+
document.getElementById('modal-name').innerText = data.name;
|
|
39
|
+
document.getElementById('modal-role').innerText = data.role;
|
|
40
|
+
document.getElementById('modal-img').src = data.image;
|
|
41
|
+
document.getElementById('modal-primary').innerText = data.primary || "Unknown";
|
|
42
|
+
document.getElementById('modal-stratagem').innerText = data.stratagem || "Unknown";
|
|
43
|
+
document.getElementById('modal-missions').innerText = data.missions || "0";
|
|
44
|
+
document.getElementById('modal-kd').innerText = data.kd || "0.0";
|
|
45
|
+
document.getElementById('modal-specialty').innerText = data.specialty || "None";
|
|
46
|
+
|
|
47
|
+
// --- 2. Populate Bio (NEW) ---
|
|
48
|
+
const bioElement = document.getElementById('modal-bio');
|
|
49
|
+
if (bioElement) {
|
|
50
|
+
bioElement.innerText = data.bio || "No service record available.";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- 3. Populate Medals (NEW) ---
|
|
54
|
+
const medalsContainer = document.getElementById('modal-medals');
|
|
55
|
+
if (medalsContainer && data.medals) {
|
|
56
|
+
medalsContainer.innerHTML = ''; // Clear old content
|
|
57
|
+
data.medals.forEach(medal => {
|
|
58
|
+
const badge = document.createElement('span');
|
|
59
|
+
badge.className = 'medal-badge';
|
|
60
|
+
badge.innerText = medal;
|
|
61
|
+
medalsContainer.appendChild(badge);
|
|
62
|
+
});
|
|
63
|
+
} else if (medalsContainer) {
|
|
64
|
+
medalsContainer.innerHTML = '<span style="color:#666; font-style:italic;">No decorations recorded.</span>';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- 4. Show Modal Animation ---
|
|
68
|
+
const modal = document.getElementById('leader-modal');
|
|
69
|
+
modal.style.display = 'flex';
|
|
70
|
+
|
|
71
|
+
// Force browser reflow to ensure transition triggers
|
|
72
|
+
void modal.offsetWidth;
|
|
73
|
+
|
|
74
|
+
modal.style.opacity = '1';
|
|
75
|
+
modal.querySelector('.modal-terminal').style.transform = 'scale(1)';
|
|
76
|
+
|
|
77
|
+
// Prevent background scrolling
|
|
78
|
+
document.body.style.overflow = 'hidden';
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Define Close Function
|
|
82
|
+
window.closeLeaderProfile = function(event) {
|
|
83
|
+
// Only close if clicking the overlay background or the X button
|
|
84
|
+
if (event.target.id === 'leader-modal' || event.target.classList.contains('modal-close')) {
|
|
85
|
+
const modal = document.getElementById('leader-modal');
|
|
86
|
+
|
|
87
|
+
// Fade out
|
|
88
|
+
modal.style.opacity = '0';
|
|
89
|
+
modal.querySelector('.modal-terminal').style.transform = 'scale(0.9)';
|
|
90
|
+
|
|
91
|
+
// Wait for transition to finish before hiding display
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
modal.style.display = 'none';
|
|
94
|
+
document.body.style.overflow = 'auto'; // Restore scrolling
|
|
95
|
+
console.log("✅ Modal closed, clicks restored.");
|
|
96
|
+
}, 300); // Must match CSS transition time
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Escape Key Listener
|
|
101
|
+
document.addEventListener('keydown', (e) => {
|
|
102
|
+
if (e.key === "Escape") {
|
|
103
|
+
const modal = document.getElementById('leader-modal');
|
|
104
|
+
if (modal.style.display === 'flex') {
|
|
105
|
+
// Simulate a click on the overlay to trigger close logic
|
|
106
|
+
window.closeLeaderProfile({ target: { id: 'leader-modal' } });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|