feat(ecma): Base game done.

This commit is contained in:
KyMAN 2026-10-16 16:02:29 +02:00
parent 5dcb86e982
commit b892747b1a
49 changed files with 8205 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/Data
/Public/data
/Public/images/backgrounds
/Public/images/game

View File

@ -0,0 +1,102 @@
"use strict";
const Values = (function(){
const Values = function(xgravity){
const self = this;
// XGravity
this.cells = 40;
this.quality = 100;
this.position = "body";
this.class_name = "xgravity";
this.fps = 60;
// Background stars
this.bg_stars_count = 140;
this.bg_stars_size_minimum = .5;
this.bg_stars_size_maximum = 2.2;
this.bg_stars_speed_minimum = .1;
this.bg_stars_speed_maximum = .6;
this.bg_stars_direction_x = 0;
this.bg_stars_direction_y = 1;
this.bg_stars_color = "rgba(186, 230, 253, {alpha})";
// Background image
this.bg_image_url = "";
this.bg_image_pan_x = 5;
this.bg_image_pan_y = 2;
// Background procedural
this.bg_procedural_oversize = 1.5;
this.bg_procedural_far_stars = {
probablity : .96,
size : [.2, .7],
images : [
"images/game/Far-Star-1.webp",
"images/game/Far-Star-2.webp",
"images/game/Far-Star-3.webp"
]
};
this.bg_procedural_nebulas = {
probablity : .87,
size : [3, 60],
images : [
"images/game/Blue-Dust.webp",
"images/game/Cold-Nebula.webp",
"images/game/Dust.webp",
"images/game/Grey-Asteroid-Dust.webp",
"images/game/Hot-Nebula.webp",
"images/game/Violet-Dust.webp",
"images/game/Yellow-Dust.webp"
]
};
this.bg_procedural_near_stars = {
probablity : .21,
size : [.5, 1.7],
images : [
"images/game/Layer.webp",
"images/game/Layer2.webp"
],
};
this.bg_procedural_suns = {
probablity : .32,
timer : [2, 10],
size : [10, 50],
images : [
"images/game/Sun.fixed.webp",
"images/game/Red-Giant.webp"
],
}
this.bg_procedural_planets = {
probablity : .17,
size : [7, 30],
images : [
"images/game/Ice-Giant.webp",
"images/game/Gas-Giant.webp",
"images/game/Exoplanet3.webp",
"images/game/Exoplanet2.webp",
"images/game/Exoplanet.webp"
]
};
// Audio
this.audio_samples = 44100;
this.audio_default_amount = 20;
this.set = () => {
Object.keys(self).forEach(key => {
if(!["set"].includes(key))
self[key] = xgravity.settings.get(key, null, self[key]);
});
};
// Enemies
this.enemy_overflight_initial_scale = 20;
this.enemy_overflight_scale_speed = 1.8;
};
return Values;
})();

View File

@ -0,0 +1,145 @@
"use strict";
const XGravity = (function(){
const XGravity = function(){
const self = this,
screen = {x : 0, y : 0};
let thread_interval = null,
fps = 12;
this.cells = 40;
this.canvas_quality = 100;
this.item_self = document;
this.values = new Values(self);
this.settings = new SettingsManager(self);
this.i18n = new I18NManager(self);
this.audio = new AudioManager(this);
this.explosions = new ExplosionsManager(this);
this.views = {
main_menu : new MainMenuView(self),
difficulty_select : new DifficultySelectView(this),
options : new OptionsView(self),
ranking : new RankingView(self),
credits : new CreditsView(self),
fps : new FpsOverlayView(this),
game : new GameView(self),
game_over : new GameOverView(this)
};
const constructor = () => {
self.values.set();
let target, preload = setInterval(() => {
if(target = document.querySelector(self.values.position)){
clearInterval(preload);
self.item_self = target.appendChild(document.createElement("div"));
self.item_self.className = self.item_self.id = self.values.class_name;
self.item_self.setAttribute("data-cells", self.values.cells);
thread_interval = setInterval(thread, 1000 / fps);
build();
};
}, self.values.fps);
Common.SCSS(SCSS);
};
const thread = () => {
if(screen.x != self.item_self.offsetWidth || screen.y != self.item_self.offsetHeight){
screen.x = self.item_self.offsetWidth;
screen.y = self.item_self.offsetHeight;
self.item_self.style.fontSize = ((screen.x < screen.y ? screen.x : screen.y) / self.cells) + "px";
};
};
const build = () => {
self.views.main_menu.go();
};
/**
* @param {...(HTMLElement|string|Array.<any|null>)} inputs
* @returns {Array.<HTMLElement>}
* @access public
* @static
*/
this.html = (...inputs) => {
const fragment = new DocumentFragment(),
master = (
inputs[0] && (inputs[0].tagName || inputs[0].nodeName) ? inputs[0] :
typeof inputs[0] == "string" ? document.querySelector(inputs[0]) :
null),
items = [];
(master ? inputs.slice(1) : inputs).forEach(subinputs => {
if(subinputs && (subinputs.tagName || subinputs.nodeName))
fragment.appendChild(subinputs);
else if(typeof subinputs == "string")
fragment.appendChild(document.createTextNode(subinputs));
else if(subinputs instanceof Array){
const [tag, attributes, children] = subinputs.concat([null, null]).slice(0, 3),
item = document.createElement(tag);
attributes && attributes.constructor == Object && Object.entries(attributes).forEach(([key, value]) => {
if(/^on[_\-]?/i.test(key) && typeof value == "function")
item.addEventListener(key.replace(/^on[_\-]?/i, "").replace(/[^a-z0-9]+/gi, "").toLowerCase(), event => {
value(item, event);
});
else if(value !== null)
item.setAttribute(key.replace(/[^a-z0-9]+/gi, "-"), value);
});
if(children instanceof Array)
self.html(...children).forEach(child => item.appendChild(child));
else if(typeof children == "string")
item.innerHTML += children;
fragment.appendChild(item);
};
});
items.push(...fragment.childNodes);
master && master.appendChild(fragment);
return items;
};
constructor();
};
// XGravity.drone_velocity = (px, py, dx, dy, ax, ay, fx, fy, max_a = 40) => {
// // 1. Sumar o restar según la posición del cursor (atracción)
// ax += (px - dx > 0 ? 1 : -1);
// ay += (py - dy > 0 ? 1 : -1);
// // 2. Limitar el acumulador para que la órbita sea elástica pero controlable
// if(ax > max_a) ax = max_a;
// else if(ax < -max_a) ax = -max_a;
// if(ay > max_a) ay = max_a;
// else if(ay < -max_a) ay = -max_a;
// return {
// ax : ax,
// ay : ay,
// x : ax * fx,
// y : ay * fy
// };
// };
XGravity.drone_velocity = (px, py, dx, dy, vx, vy, f, delta = 0) => [
vx += (px - dx) * f,
vy += (py - dy) * f
];
return XGravity;
})();

View File

@ -0,0 +1,76 @@
"use strict";
const ImageBackground = (function(){
const ImageBackground = function(xgravity){
const self = this;
let image = null,
is_loaded = false,
offset_x = 0,
offset_y = 0;
const constructor = () => {
if(xgravity.values.bg_image_url){
image = new Image();
image.onload = () => {
is_loaded = true;
};
image.onerror = event => {
console.error("[XGravity] Error al cargar bg_image_url:", xgravity.values.bg_image_url, event);
};
image.src = xgravity.values.bg_image_url;
if(image.complete && image.naturalWidth > 0)
is_loaded = true;
};
};
this.resize = (width, height) => {};
this.render = (context, width, height, delta_time) => {
if(!is_loaded || !image || !image.naturalWidth){
context.fillStyle = "#020408";
context.fillRect(0, 0, width, height);
return;
};
// Relación de aspecto cover
const canvas_ratio = width / height,
image_ratio = image.naturalWidth / image.naturalHeight;
let draw_width, draw_height;
if(canvas_ratio > image_ratio){
draw_width = width;
draw_height = width / image_ratio;
}else{
draw_height = height;
draw_width = height * image_ratio;
};
// Si es completamente estática (como en el juego original)
if(xgravity.values.bg_image_pan_x === 0 && xgravity.values.bg_image_pan_y === 0){
context.drawImage(image, (width - draw_width) / 2, (height - draw_height) / 2, draw_width, draw_height);
return;
};
// Si tiene movimiento
const start_x = ((width - draw_width) / 2) + (offset_x = (offset_x + xgravity.values.bg_image_pan_x * delta_time) % draw_width),
start_y = ((height - draw_height) / 2) + (offset_y = (offset_y + xgravity.values.bg_image_pan_y * delta_time) % draw_height);
// Dibujar la cuadrícula de repetición necesaria para cubrir cualquier espacio
for(let x = start_x - draw_width; x < width; x += draw_width)
for(let y = start_y - draw_height; y < height; y += draw_height)
context.drawImage(image, x, y, draw_width, draw_height);
};
constructor();
};
return ImageBackground;
})();

View File

@ -0,0 +1,276 @@
"use strict";
const ProceduralBackground = (function(){
const ProceduralBackground = function(xgravity){
const self = this,
image_cache = {},
active_suns = [];
let is_ready = false,
universe_size = 0,
// Lienzos intermedios pre-renderizados
base_canvas = null,
planets_canvas = null;
const constructor = () => {
preload_images(get_all_configured_urls(), () => {
build_universe(
Math.floor(window.innerWidth * xgravity.values.quality / 100) || 800,
Math.floor(window.innerHeight * xgravity.values.quality / 100) || 600
);
});
};
const preload_images = (urls, callback) => {
let pending = urls.length;
if(pending === 0){
callback();
return;
};
urls.forEach(url => {
if(image_cache[url])
-- pending || callback();
else{
const image = new Image();
image.onload = () => {
image_cache[url] = image;
-- pending || callback();
};
image.onerror = () => {
console.warn("[XGravity] ProceduralBackground: No se pudo cargar", url);
-- pending || callback();
};
image.src = url;
};
});
};
const get_all_configured_urls = () => {
const urls = [];
for(const key of [
"bg_procedural_far_stars",
"bg_procedural_nebulas",
"bg_procedural_near_stars",
"bg_procedural_suns",
"bg_procedural_planets"
]){
const configuration = {...xgravity.values[key]};
if(configuration && configuration.images instanceof Array)
configuration.images.forEach(url => {
urls.includes(url) || urls.push(url);
});
};
return urls;
};
const get_cell_size = (width, height) => (width < height ? width : height) / xgravity.cells;
// Dibuja un elemento radialmente desde el centro hacia afuera
const draw_element_from_center = (target_context, image, center_x, center_y, maximum_radius, size_range, cell_px) => {
const display_angle = Math.random() * Math.PI * 2,
distance = Math.sqrt(Math.random()) * maximum_radius, // Distribución de área uniforme
// Tamaño en celdas
size_minimum = size_range ? size_range[0] : 10,
target_px = (Math.random() * ((size_range ? size_range[1] : size_minimum) - size_minimum) + size_minimum) * cell_px,
// Mantener proporción de aspecto
image_ratio = image.naturalWidth / image.naturalHeight,
[width, height] = image_ratio >= 1 ? [target_px, target_px / image_ratio] : [target_px / image_ratio, target_px];
target_context.save();
target_context.translate(
center_x + Math.cos(display_angle) * distance,
center_y + Math.sin(display_angle) * distance
);
target_context.rotate(Math.random() * Math.PI * 2);
target_context.drawImage(image, -width / 2, -height / 2, width, height);
target_context.restore();
};
const build_universe = (width, height) => {
const cell_px = get_cell_size(width, height),
center_x = (universe_size = Math.floor(Math.max(width, height) * 1.5)) / 2,
center_y = universe_size / 2,
maximum_radius = universe_size * 0.45,
base_context = (base_canvas = document.createElement("canvas")).getContext("2d"),
planets_context = (planets_canvas = document.createElement("canvas")).getContext("2d");
let count;
// 1. Offscreen base (Capas 1, 2 y 3)
base_canvas.width = universe_size;
base_canvas.height = universe_size;
base_context.fillStyle = "#020408";
base_context.fillRect(0, 0, universe_size, universe_size);
for(const partial_key of ["far_stars", "nebulas", "near_stars"]){
const key = "bg_procedural_" + partial_key;
count = 0;
while(Math.random() <= (xgravity.values[key].probablity || 0) && count ++ < ProceduralBackground.MAXIMUM_ITERATIONS){
const chosen = xgravity.values[key].images[Math.floor(Math.random() * xgravity.values[key].images.length)];
image_cache[chosen] &&
draw_element_from_center(base_context, image_cache[chosen], center_x, center_y, maximum_radius * .5, xgravity.values[key].size, cell_px);
};
};
// 2. Offscreen Capa 5: Planetas (Bucle While)
planets_canvas.width = universe_size;
planets_canvas.height = universe_size;
// const planets_context = (planets_canvas = document.createElement("canvas")).getContext("2d");
count = 0;
while(Math.random() <= (xgravity.values.bg_procedural_planets.probablity || 0) && count ++ < ProceduralBackground.MAXIMUM_ITERATIONS){
const chosen = xgravity.values.bg_procedural_planets.images[Math.floor(Math.random() * xgravity.values.bg_procedural_planets.images.length)];
image_cache[chosen] &&
draw_element_from_center(planets_context, image_cache[chosen], center_x, center_y, maximum_radius, xgravity.values.bg_procedural_planets.size, cell_px);
};
// 3. Capa 4: Soles vivos con carrusel de 3 sprites
active_suns.length = 0;
count = 0;
while(Math.random() <= (xgravity.values.bg_procedural_suns.probablity || 0) && count ++ < ProceduralBackground.MAXIMUM_ITERATIONS){
const chosen = xgravity.values.bg_procedural_suns.images[Math.floor(Math.random() * xgravity.values.bg_procedural_suns.images.length)];
if(image_cache[chosen]){
const timer_range = xgravity.values.bg_procedural_suns.timer || [2, 10],
size_minimum = xgravity.values.bg_procedural_suns.size ? xgravity.values.bg_procedural_suns.size[0] : 10,
sun_px = (Math.random() * ((
xgravity.values.bg_procedural_suns.size ? xgravity.values.bg_procedural_suns.size[1] :
size_minimum) - size_minimum) + size_minimum) * cell_px,
ratio = image_cache[chosen].naturalWidth / image_cache[chosen].naturalHeight,
display_angle = Math.random() * Math.PI * 2,
distance = Math.random() * (maximum_radius * .65);
active_suns.push({
image : image_cache[chosen],
offset_x : Math.cos(display_angle) * distance,
offset_y : Math.sin(display_angle) * distance,
w : ratio >= 1 ? sun_px : sun_px * ratio,
h : ratio >= 1 ? sun_px / ratio : sun_px,
// Array ordenado: [0]=fondo, [1]=medio, [2]=frente
sprites : [
{ angle : Math.random() * Math.PI * 2, alpha : 1.0 }, // Fondo: baja de 1 a 0
{ angle : Math.random() * Math.PI * 2, alpha : 1.0 }, // Medio: base opaca constante a 1
{ angle : Math.random() * Math.PI * 2, alpha : 0.0 } // Frente: sube de 0 a 1
],
duration : Math.random() * (timer_range[1] - timer_range[0]) + timer_range[0],
elapsed : 0,
timer_range : timer_range
});
};
};
is_ready = true;
};
this.resize = (width, height) => {
is_ready || build_universe(width, height);
};
this.render = (context, width, height, delta_time) => {
if(!is_ready || !base_canvas){
context.fillStyle = "#020408";
context.fillRect(0, 0, width, height);
return;
};
const screen_cx = width / 2,
screen_cy = height / 2,
draw_x = screen_cx - (universe_size / 2),
draw_y = screen_cy - (universe_size / 2);
// 1. Capas 1, 2 y 3 pre-renderizadas
context.drawImage(base_canvas, draw_x, draw_y);
// 2. Capa 4: Renderizado de soles vivos
for(let i = 0; i < active_suns.length; i ++){
const sun = active_suns[i],
progress = Math.min((sun.elapsed += delta_time) / sun.duration, 1);
// Sprite 0 (fondo) baja de 1 a 0
sun.sprites[0].alpha = 1.0 - progress;
// Sprite 1 (medio) se mantiene siempre sólido al 100%
sun.sprites[1].alpha = 1.0;
// Sprite 2 (frente) sube de 0 a 1 al unísono
sun.sprites[2].alpha = progress;
// Al completar el tiempo
if(progress >= 1){
// 1. Extraemos el sprite del fondo (que ya está a alpha 0)
const old_bottom = sun.sprites.shift();
// 2. Le asignamos un nuevo ángulo aleatorio (totalmente invisible porque alpha = 0)
old_bottom.angle = Math.random() * Math.PI * 2;
old_bottom.alpha = 0.0;
// 3. Pasa al frente de la pila (nuevo Sprite 2)
sun.sprites.push(old_bottom);
// 4. Reiniciamos temporizador con nueva duración aleatoria
sun.elapsed = 0;
sun.duration = Math.random() * (sun.timer_range[1] - sun.timer_range[0]) + sun.timer_range[0];
};
// Dibujar los 3 sprites en orden estricto de profundidad
// for(let sprite = 0; sprite < sun.sprites.length; sprite++){
for(const sprite of sun.sprites)
// const sp = sun.sprites[s];
if(sprite.alpha > 0){
context.save();
context.globalAlpha = sprite.alpha;
context.translate(screen_cx + sun.offset_x, screen_cy + sun.offset_y);
context.rotate(sprite.angle);
context.drawImage(sun.image, -sun.w / 2, -sun.h / 2, sun.w, sun.h);
context.restore();
};
context.globalAlpha = 1.0;
};
// 3. Capa 5: Planetas pre-renderizados
planets_canvas && context.drawImage(planets_canvas, draw_x, draw_y);
};
constructor();
};
// Límite de seguridad por capa para evitar bucles infinitos en caso de probablity >= 1
ProceduralBackground.MAXIMUM_ITERATIONS = 50;
return ProceduralBackground;
})();

View File

@ -0,0 +1,66 @@
"use strict";
const StarsBackground = (function(){
const StarsBackground = function(xgravity){
const self = this;
const stars = [];
const init_stars = (width, height) => {
stars.length = 0;
for(let i = 0; i < xgravity.values.bg_stars_count; i++)
stars.push({
x : Math.random() * width,
y : Math.random() * height,
size : Math.random() * (xgravity.values.bg_stars_size_maximum - xgravity.values.bg_stars_size_minimum) + xgravity.values.bg_stars_size_minimum,
speed : Math.random() * (xgravity.values.bg_stars_speed_maximum - xgravity.values.bg_stars_speed_minimum) + xgravity.values.bg_stars_speed_minimum,
alpha : Math.random() * 0.8 + 0.2
});
};
this.resize = (width, height) => {
init_stars(width, height);
};
this.render = (ctx, width, height, delta_time) => {
ctx.fillStyle = "#020408";
ctx.fillRect(0, 0, width, height);
stars.length || init_stars(width, height);
for(const star of stars){
star.x += xgravity.values.bg_stars_direction_x * star.speed * (delta_time * 60);
star.y += xgravity.values.bg_stars_direction_y * star.speed * (delta_time * 60);
if(star.y > height){
star.y = 0;
star.x = Math.random() * width;
}else if(star.y < 0){
star.y = height;
star.x = Math.random() * width;
};
if(star.x > width){
star.x = 0;
star.y = Math.random() * height;
}else if(star.x < 0){
star.x = width;
star.y = Math.random() * height;
};
ctx.fillStyle = Common.string_variables(xgravity.values.bg_stars_color, {
alpha : star.alpha.toFixed(2)
});
ctx.fillRect(star.x, star.y, star.size, star.size);
};
};
};
return StarsBackground;
})();

View File

@ -0,0 +1,87 @@
"use strict";
const CockpitImpactFx = (function(){
const CockpitImpactFx = function(xgravity){
let flash_alpha = 0;
let shards = [];
this.trigger = () => {
flash_alpha = 0.55; // Fogonazo rápido sobre el cristal
shards = []; // Sin metralla de piedras
};
this.update = (delta_time) => {
if(flash_alpha > 0){
flash_alpha = Math.max(0, flash_alpha - (3.2 * delta_time));
}
for(let i = shards.length - 1; i >= 0; i--){
const s = shards[i];
s.life -= s.decay * delta_time;
if(s.life <= 0){
shards.splice(i, 1);
continue;
}
s.x += s.vx * delta_time;
s.y += s.vy * delta_time;
s.angle += s.angular_vel * delta_time;
s.vx *= 0.96;
s.vy *= 0.96;
}
};
this.render = (ctx, width, height) => {
const cx = width / 2;
const cy = height / 2;
// Flash blanco en pantalla completa
if(flash_alpha > 0){
ctx.save();
ctx.fillStyle = "#ffffff";
ctx.globalAlpha = Math.min(0.7, flash_alpha);
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
// Esquirlas de roca sólidas
if(shards.length > 0){
ctx.save();
ctx.translate(cx, cy);
for(let i = 0; i < shards.length; i++){
const s = shards[i];
ctx.save();
ctx.translate(s.x, s.y);
ctx.rotate(s.angle);
ctx.fillStyle = s.color;
ctx.globalAlpha = Math.max(0, s.life);
ctx.beginPath();
ctx.moveTo(s.vertices[0].x, s.vertices[0].y);
for(let v = 1; v < s.vertices.length; v++){
ctx.lineTo(s.vertices[v].x, s.vertices[v].y);
}
ctx.closePath();
ctx.fill();
ctx.lineWidth = 1.0;
ctx.strokeStyle = "rgba(15, 23, 42, 0.85)";
ctx.stroke();
ctx.restore();
}
ctx.restore();
}
};
this.reset = () => {
flash_alpha = 0;
shards = [];
};
};
return CockpitImpactFx;
})();

View File

@ -0,0 +1,77 @@
"use strict";
const DeflectionSparksFx = (function(){
const DeflectionSparksFx = function(xgravity){
let sparks = [];
this.add = (cell_x, cell_y, normal_x, normal_y, intensity = 1.0) => {
const base_count = Math.floor(10 + (intensity * 25));
const count = Math.min(60, base_count);
const base_angle = Math.atan2(normal_y, normal_x);
for(let i = 0; i < count; i++){
const spread = (Math.random() - 0.5) * (Math.PI * 0.7);
const spark_angle = base_angle + spread;
const speed = (Math.random() * 12 + 6) * Math.min(2.5, intensity);
const life = (Math.random() * 0.18) + 0.08;
sparks.push({
x : cell_x,
y : cell_y,
vx : Math.cos(spark_angle) * speed,
vy : Math.sin(spark_angle) * speed,
len : (Math.random() * 0.3) + 0.15,
life : life,
max_life : life,
color : Math.random() > 0.3 ? "#fef08a" : "#f97316"
});
}
};
this.update = (delta_time) => {
for(let i = sparks.length - 1; i >= 0; i--){
const s = sparks[i];
s.life -= delta_time;
if(s.life <= 0){
sparks.splice(i, 1);
continue;
}
s.x += s.vx * delta_time;
s.y += s.vy * delta_time;
s.vx *= 0.90;
s.vy *= 0.90;
}
};
this.render = (ctx, cell_px, cx, cy) => {
for(let i = 0; i < sparks.length; i++){
const s = sparks[i];
const px = cx + (s.x * cell_px);
const py = cy + (s.y * cell_px);
const progress = Math.max(0, s.life / s.max_life);
const spd = Math.sqrt(s.vx * s.vx + s.vy * s.vy);
const dir_x = spd > 0.001 ? s.vx / spd : 0;
const dir_y = spd > 0.001 ? s.vy / spd : 0;
const tail_length_px = (s.len || 0.25) * cell_px * Math.min(2.5, spd * 0.15);
ctx.lineWidth = cell_px * 0.06;
ctx.strokeStyle = s.color;
ctx.globalAlpha = progress;
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(px - (dir_x * tail_length_px), py - (dir_y * tail_length_px));
ctx.stroke();
}
};
this.reset = () => {
sparks = [];
};
};
return DeflectionSparksFx;
})();

View File

@ -0,0 +1,134 @@
"use strict";
const HullHitExplosionsFx = (function(){
const HullHitExplosionsFx = function(xgravity){
let shockwaves = [];
let particles = [];
this.add = (cell_x, cell_y) => {
if(xgravity.audio){
xgravity.audio.play("hit_explosion");
}
const particles_range = xgravity.settings.get("explosion_particles_range", null, [45, 75]);
const spread_reach = xgravity.settings.get("explosion_particles_spread", null, 11.0);
const base_shockwave_radius = xgravity.settings.get("explosion_shockwave_radius", null, 4.5);
const min_p = Math.min(particles_range[0], particles_range[1]);
const max_p = Math.max(particles_range[0], particles_range[1]);
const count = Math.floor(min_p + Math.random() * (max_p - min_p + 1));
// Onda roja más grande
shockwaves.push({
x : cell_x,
y : cell_y,
radius : 0.4,
max_radius : base_shockwave_radius * 1.15,
speed : 8.0,
angle : Math.random() * Math.PI * 2,
aspect_ratio : 0.42 + (Math.random() * 0.18),
alpha : 1.0,
color_rgb : "239, 68, 68"
});
// Metralla naranja/roja/amarilla
for(let i = 0; i < count; i++){
const angle = Math.random() * Math.PI * 2;
const speed = (Math.random() * spread_reach) + 1.2;
const life = (Math.random() * 0.38) + 0.28;
const rim_rgb = Math.random() > 0.4 ? "239, 68, 68" : (Math.random() > 0.5 ? "249, 115, 22" : "251, 191, 36");
particles.push({
x : cell_x,
y : cell_y,
vx : Math.cos(angle) * speed,
vy : Math.sin(angle) * speed,
size : (Math.random() * 0.16) + 0.08,
life : life,
max_life : life,
core_rgb : "255, 255, 255",
rim_rgb : rim_rgb
});
}
};
this.update = (delta_time) => {
for(let i = shockwaves.length - 1; i >= 0; i--){
const s = shockwaves[i];
s.radius += s.speed * delta_time;
s.alpha = Math.max(0, 1.0 - (s.radius / s.max_radius));
if(s.radius >= s.max_radius){
shockwaves.splice(i, 1);
}
}
for(let i = particles.length - 1; i >= 0; i--){
const p = particles[i];
p.life -= delta_time;
if(p.life <= 0){
particles.splice(i, 1);
continue;
}
p.x += p.vx * delta_time;
p.y += p.vy * delta_time;
p.vx *= 0.93;
p.vy *= 0.93;
}
};
this.render = (ctx, cell_px, cx, cy) => {
for(let i = 0; i < shockwaves.length; i++){
const s = shockwaves[i];
const px = cx + (s.x * cell_px);
const py = cy + (s.y * cell_px);
const r_px = s.radius * cell_px;
ctx.save();
ctx.translate(px, py);
ctx.rotate(s.angle);
ctx.scale(1.0, s.aspect_ratio);
ctx.lineWidth = cell_px * 0.14 * s.alpha;
ctx.strokeStyle = `rgba(${s.color_rgb}, ${s.alpha * 0.85})`;
ctx.beginPath();
ctx.arc(0, 0, r_px, 0, Math.PI * 2);
ctx.stroke();
ctx.lineWidth = cell_px * 0.05 * s.alpha;
ctx.strokeStyle = `rgba(255, 255, 255, ${s.alpha * 0.6})`;
ctx.beginPath();
ctx.arc(0, 0, r_px * 0.82, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
for(let i = 0; i < particles.length; i++){
const p = particles[i];
const px = cx + (p.x * cell_px);
const py = cy + (p.y * cell_px);
const r_px = p.size * cell_px;
const progress = Math.max(0, p.life / p.max_life);
const grad = ctx.createRadialGradient(px, py, 0, px, py, r_px);
grad.addColorStop(0.0, `rgba(${p.core_rgb}, ${progress})`);
grad.addColorStop(0.45, `rgba(${p.rim_rgb}, ${progress * 0.75})`);
grad.addColorStop(1.0, `rgba(${p.rim_rgb}, 0.0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(px, py, r_px, 0, Math.PI * 2);
ctx.fill();
}
};
this.reset = () => {
shockwaves = [];
particles = [];
};
};
return HullHitExplosionsFx;
})();

View File

@ -0,0 +1,138 @@
"use strict";
const ShieldExplosionsFx = (function(){
const ShieldExplosionsFx = function(xgravity){
let shockwaves = [];
let particles = [];
this.add = (cell_x, cell_y) => {
if(xgravity.audio){
xgravity.audio.play("shield_explosion");
}
const particles_range = xgravity.settings.get("explosion_particles_range", null, [45, 75]);
const spread_reach = xgravity.settings.get("explosion_particles_spread", null, 11.0);
const base_shockwave_radius = xgravity.settings.get("explosion_shockwave_radius", null, 4.5);
const min_p = Math.min(particles_range[0], particles_range[1]);
const max_p = Math.max(particles_range[0], particles_range[1]);
const count = Math.floor(min_p + Math.random() * (max_p - min_p + 1));
// Onda elíptica azulada
shockwaves.push({
x : cell_x,
y : cell_y,
radius : 0.4,
max_radius : base_shockwave_radius,
speed : 9.5,
angle : Math.random() * Math.PI * 2,
aspect_ratio : 0.42 + (Math.random() * 0.18),
alpha : 1.0,
color_rgb : "56, 189, 248"
});
// Micropartículas de plasma azul
for(let i = 0; i < count; i++){
const angle = Math.random() * Math.PI * 2;
const speed = (Math.random() * spread_reach) + 1.2;
const life = (Math.random() * 0.38) + 0.28;
const rim_rgb = Math.random() > 0.4 ? "56, 189, 248" : (Math.random() > 0.5 ? "186, 230, 253" : "2, 132, 199");
particles.push({
x : cell_x,
y : cell_y,
vx : Math.cos(angle) * speed,
vy : Math.sin(angle) * speed,
size : (Math.random() * 0.16) + 0.08,
life : life,
max_life : life,
core_rgb : "255, 255, 255",
rim_rgb : rim_rgb
});
}
};
this.update = (delta_time) => {
// Ondas
for(let i = shockwaves.length - 1; i >= 0; i--){
const s = shockwaves[i];
s.radius += s.speed * delta_time;
s.alpha = Math.max(0, 1.0 - (s.radius / s.max_radius));
if(s.radius >= s.max_radius){
shockwaves.splice(i, 1);
}
}
// Partículas
for(let i = particles.length - 1; i >= 0; i--){
const p = particles[i];
p.life -= delta_time;
if(p.life <= 0){
particles.splice(i, 1);
continue;
}
p.x += p.vx * delta_time;
p.y += p.vy * delta_time;
p.vx *= 0.93;
p.vy *= 0.93;
}
};
this.render = (ctx, cell_px, cx, cy) => {
// Ondas
for(let i = 0; i < shockwaves.length; i++){
const s = shockwaves[i];
const px = cx + (s.x * cell_px);
const py = cy + (s.y * cell_px);
const r_px = s.radius * cell_px;
ctx.save();
ctx.translate(px, py);
ctx.rotate(s.angle);
ctx.scale(1.0, s.aspect_ratio);
ctx.lineWidth = cell_px * 0.14 * s.alpha;
ctx.strokeStyle = `rgba(${s.color_rgb}, ${s.alpha * 0.85})`;
ctx.beginPath();
ctx.arc(0, 0, r_px, 0, Math.PI * 2);
ctx.stroke();
ctx.lineWidth = cell_px * 0.05 * s.alpha;
ctx.strokeStyle = `rgba(255, 255, 255, ${s.alpha * 0.6})`;
ctx.beginPath();
ctx.arc(0, 0, r_px * 0.82, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
// Partículas
for(let i = 0; i < particles.length; i++){
const p = particles[i];
const px = cx + (p.x * cell_px);
const py = cy + (p.y * cell_px);
const r_px = p.size * cell_px;
const progress = Math.max(0, p.life / p.max_life);
const grad = ctx.createRadialGradient(px, py, 0, px, py, r_px);
grad.addColorStop(0.0, `rgba(${p.core_rgb}, ${progress})`);
grad.addColorStop(0.45, `rgba(${p.rim_rgb}, ${progress * 0.75})`);
grad.addColorStop(1.0, `rgba(${p.rim_rgb}, 0.0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(px, py, r_px, 0, Math.PI * 2);
ctx.fill();
}
};
this.reset = () => {
shockwaves = [];
particles = [];
};
};
return ShieldExplosionsFx;
})();

View File

@ -0,0 +1,119 @@
"use strict";
const AudioManager = (function(){
const AudioManager = function(xgravity){
const self = this;
let context = null;
let sfx_bus = null;
let is_muted = false;
const sound_registry = {};
const init_audio = () => {
if(!context){
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if(AudioContextClass){
// Cadena Master: Bus -> Saturador -> Compresor/Limitador -> Altavoces
const limiter = (context = new AudioContextClass()).createDynamicsCompressor(),
saturator = context.createWaveShaper();
limiter.threshold.setValueAtTime(-4, context.currentTime);
limiter.knee.setValueAtTime(6, context.currentTime);
limiter.ratio.setValueAtTime(16, context.currentTime);
limiter.attack.setValueAtTime(0.003, context.currentTime);
limiter.release.setValueAtTime(0.18, context.currentTime);
saturator.curve = make_distortion_curve(15);
saturator.oversample = "2x";
sfx_bus = context.createGain();
sfx_bus.gain.setValueAtTime(1.0, context.currentTime);
sfx_bus.connect(saturator);
saturator.connect(limiter);
limiter.connect(context.destination);
};
};
context && context.state === "suspended" && context.resume();
};
const make_distortion_curve = (amount = null) => {
const curve = new Float32Array(xgravity.values.audio_samples);
const degrees = Math.PI / 180;
if(amount === null)
amount = xgravity.values.audio_default_amount;
for(let i = 0; i < xgravity.values.audio_samples; ++ i){
const x = (i * 2) / xgravity.values.audio_samples - 1;
curve[i] = ((3 + amount) * x * 20 * degrees) / (Math.PI + amount * Math.abs(x));
};
return curve;
};
this.register_sound = (id, generator_fn) => {
sound_registry[id] = generator_fn;
};
this.play = (sound_id, custom_volume = null) => {
if(is_muted) return;
init_audio();
if(!context || !sfx_bus) return;
const generator = sound_registry[sound_id];
if(typeof generator === "function"){
const master_vol = custom_volume !== null
? custom_volume
: xgravity.settings.get("explosion_volume", null, 1.0);
generator(context, sfx_bus, master_vol);
}
};
this.set_mute = (state) => {
is_muted = Boolean(state);
if(sfx_bus && context){
sfx_bus.gain.setValueAtTime(is_muted ? 0 : 1.0, context.currentTime);
}
};
const constructor = () => {
if(typeof ShieldExplosionSound !== "undefined"){
this.register_sound("shield_explosion", ShieldExplosionSound);
}
if(typeof HitExplosionSound !== "undefined"){
this.register_sound("hit_explosion", HitExplosionSound);
}
if(typeof ShieldLowAlarmSound !== "undefined"){
this.register_sound("shield_low_alarm", ShieldLowAlarmSound);
}
if(typeof PerimeterWarningSound !== "undefined"){
this.register_sound("perimeter_warning", PerimeterWarningSound);
}
if(typeof MothershipImpactSound !== "undefined"){
this.register_sound("mothership_impact", MothershipImpactSound);
}
if(typeof AsteroidThreatAlarmSound !== "undefined"){
this.register_sound("asteroid_threat", AsteroidThreatAlarmSound);
}
};
constructor();
};
return AudioManager;
})();

View File

@ -0,0 +1,748 @@
"use strict";
const EnemiesManager = (function(){
const ENEMY_FACTORY = {
"static_mine" : StaticMineEnemyModel,
"bouncer" : typeof BouncerEnemyModel !== "undefined" ? BouncerEnemyModel : null,
"chaser" : ChaserEnemyModel,
"rusher" : typeof RusherEnemyModel !== "undefined" ? RusherEnemyModel : null
};
const EnemiesManager = function(xgravity){
const self = this;
let active_enemies = [];
let damage_popups = [];
let spawn_clock = 0;
let game_clock = 0;
let on_mothership_impact = null;
let total_enemies_spawned = 0;
// Caché centralizado de texturas
const sprite_cache = {};
// ----------------------------------------------------
// EXPLOSIONES DE NEBULOSA CONTRA LA NAVE NODRIZA
// ----------------------------------------------------
let mothership_explosions = [];
const nebula_image_cache = {};
let threat_beep_timer = 0;
this.get_total_spawned = () => total_enemies_spawned;
// Identifica si la entidad es una nave lanzadera
const is_thrower_enemy = (enemy, state) => {
const raw = [
enemy.constructor ? enemy.constructor.name : "",
typeof enemy.get_type === "function" ? enemy.get_type() : "",
enemy.type || "",
state.type || "",
enemy.config ? (enemy.config.type || enemy.config.behavior || enemy.config.id || enemy.config.name || "") : ""
].join(" ").toLowerCase();
return raw.includes("thrower") || raw.includes("lanzador") || raw.includes("lanzadera");
};
// Identifica si la entidad es un asteroide (excluyendo a la nave lanzadora)
const is_asteroid_rock = (enemy, state) => {
const raw = [
enemy.constructor ? enemy.constructor.name : "",
typeof enemy.get_type === "function" ? enemy.get_type() : "",
enemy.type || "",
state.type || "",
enemy.config ? (enemy.config.type || enemy.config.behavior || enemy.config.id || enemy.config.name || "") : ""
].join(" ").toLowerCase();
const is_thrower = raw.includes("thrower") || raw.includes("lanzador") || raw.includes("lanzadera");
const is_rock = raw.includes("asteroid") || raw.includes("asteroide") || raw.includes("rock");
return is_rock && !is_thrower;
};
// Calcula el nivel de riesgo de 0.0 a 1.0 según lanzaderas y piedras en pantalla
const calculate_asteroid_threat_level = () => {
let throwers_count = 0;
let asteroids_count = 0;
let incoming_progress = 0;
for(let i = 0; i < active_enemies.length; i++){
const enemy = active_enemies[i];
const state = enemy.get_state();
if(!state.alive) continue; // Solo entidades vivas en juego
// 1. ¿Hay ya un asteroide lanzado hacia la cabina?
const is_heading_to_mothership = typeof enemy.is_targeted_towards_mothership === "function"
&& enemy.is_targeted_towards_mothership();
if(is_heading_to_mothership){
const current_size = (state.size || 2.4) * (state.scale || 1.0);
incoming_progress = Math.max(incoming_progress, Math.min(1.0, current_size / 40.0));
continue;
}
// 2. Conteo de amenazas activas en pantalla
if(is_thrower_enemy(enemy, state)){
throwers_count++;
} else if(is_asteroid_rock(enemy, state)){
asteroids_count++;
}
}
// CRÍTICO: Si ya viene una roca hacia la pantalla, la alarma tiene máxima prioridad
if(incoming_progress > 0){
return 0.75 + (incoming_progress * 0.25); // 0.75 a 1.0
}
// CONDICIÓN PRINCIPAL: Solo hay riesgo si coinciden lanzadores Y asteroides
if(throwers_count > 0 && asteroids_count > 0){
// Cálculo de riesgo proporcional:
// Base de 0.25 por la combinación, sumando peso por cada lanzador y piedra extra
const threat = 0.25 + ((throwers_count - 1) * 0.15) + ((asteroids_count - 1) * 0.08);
return Math.min(0.70, threat); // Tope de 0.70 antes de que el proyectil sea disparado
}
// Si hay asteroides solos o lanzadores sin piedras: Silencio
return 0;
};
const get_nebula_image = (src) => {
if(!src) return null;
if(!nebula_image_cache[src]){
const img = new Image();
img.src = src;
nebula_image_cache[src] = img;
}
return nebula_image_cache[src];
};
const spawn_mothership_nebula_explosion = () => {
const nebula_list = (xgravity.values && xgravity.values.bg_procedural_nebulas && Array.isArray(xgravity.values.bg_procedural_nebulas.images))
? xgravity.values.bg_procedural_nebulas.images
: [];
if(nebula_list.length === 0) return;
// Elegir una nebulosa aleatoria
const random_src = nebula_list[Math.floor(Math.random() * nebula_list.length)];
const img = get_nebula_image(random_src);
mothership_explosions.push({
image : img,
size : 50.0, // Tamaño inicial: 50 celdas
target_size : 80.0, // Tamaño final: 80 celdas
growth_speed : 36.0,// Velocidad de expansión (dura ~0.85s)
angle : Math.random() * Math.PI * 2, // Giro aleatorio inicial
rot_speed : (Math.random() - 0.5) * 0.35, // Leve rotación durante la expansión
alpha : 1.0
});
};
this.set_on_mothership_impact = (fn) => { on_mothership_impact = fn; };
const get_cached_image = (src) => {
if(!src) return null;
if(!sprite_cache[src]){
const img = new Image();
img.src = src;
sprite_cache[src] = img;
}
return sprite_cache[src];
};
const get_cell_size = (width, height) => {
const cells = xgravity.settings.get("cells", null, 40);
return (width < height ? width : height) / cells;
};
this.reset = () => {
active_enemies = [];
damage_popups = [];
spawn_clock = 0;
game_clock = 0;
};
const get_unlocked_threshold = () => {
const min_th = xgravity.settings.get("enemy_min_threshold", null, 0.04);
const max_th = xgravity.settings.get("enemy_max_threshold", null, 0.11);
const ramp_time = xgravity.settings.get("enemy_time_to_max_difficulty", null, 120);
const progress = Math.min(game_clock / ramp_time, 1.0);
return min_th + ((max_th - min_th) * progress);
};
// const spawn_enemy_instance = (cfg) => {
// const EnemyClass = ENEMY_FACTORY[cfg.id];
// if(EnemyClass){
// if(cfg.image) get_cached_image(cfg.image); // Pre-calentamiento de caché
// active_enemies.push(new EnemyClass(xgravity, cfg));
// }
// };
const spawn_enemy_instance = (config) => {
let enemy = null;
switch(config.id){
case "static_mine":
enemy = new StaticMineEnemyModel(xgravity, config);
break;
case "bouncer":
enemy = new BouncerEnemyModel(xgravity, config);
break;
case "chaser":
enemy = new ChaserEnemyModel(xgravity, config);
break;
case "rusher":
enemy = new RusherEnemyModel(xgravity, config);
break;
case "asteroid":
enemy = new AsteroidEnemyModel(xgravity, config);
break;
case "asteroid_thrower":
enemy = new AsteroidThrowerEnemyModel(xgravity, config);
break;
}
if(enemy){
active_enemies.push(enemy);
total_enemies_spawned++;
}
};
const get_current_threshold = () => {
const min_th = xgravity.settings.get_effective_stat("base_enemy_min_threshold", "min_threshold_mult");
const max_th = xgravity.settings.get_effective_stat("base_enemy_max_threshold", "max_threshold_mult");
const half_life = xgravity.settings.get("difficulty_ramp_half_life", null, 90.0);
const ramp_power = xgravity.settings.get("difficulty_ramp_power", null, 1.25);
const preset = xgravity.settings.get_difficulty_preset();
const ramp_mult = preset ? preset.ramp_speed_mult : 1.0;
// Curva asintótica: ratio / (1 + ratio) -> tiende asintóticamente a 1.0 sin sobrepasarlo
const effective_time = game_clock * ramp_mult;
const ratio = Math.pow(effective_time / half_life, ramp_power);
const asymptotic_progress = ratio / (1.0 + ratio);
return min_th + ((max_th - min_th) * asymptotic_progress);
};
const process_spawner = (delta_time) => {
const interval = xgravity.settings.get_effective_stat("base_enemy_spawn_interval", "spawn_interval_mult");
spawn_clock += delta_time;
if(spawn_clock >= interval){
spawn_clock = 0;
const roll = Math.random();
const current_threshold = get_current_threshold();
if(roll <= current_threshold){
const roster = xgravity.settings.get("enemies_roster", null, []);
for(let i = 0; i < roster.length; i++){
const cfg = roster[i];
if(roll >= cfg.range[0] && roll < cfg.range[1]){
spawn_enemy_instance(cfg);
break;
}
}
}
}
};
// const process_spawner = (delta_time) => {
// const interval = xgravity.settings.get("enemy_spawn_interval", null, 0.6);
// spawn_clock += delta_time;
// if(spawn_clock >= interval){
// spawn_clock = 0;
// const roll = Math.random();
// const current_threshold = get_unlocked_threshold();
// if(roll <= current_threshold){
// const roster = xgravity.settings.get("enemies_roster", null, []);
// for(let i = 0; i < roster.length; i++){
// const cfg = roster[i];
// if(roll >= cfg.range[0] && roll < cfg.range[1]){
// spawn_enemy_instance(cfg);
// break;
// }
// }
// }
// }
// };
const spawn_damage_popup = (cell_x, cell_y, amount) => {
const duration = xgravity.settings.get("enemy_damage_popup_duration", null, 0.75);
damage_popups.push({
x : cell_x,
y : cell_y,
vx : (Math.random() - 0.5) * 1.5,
vy : -3.2,
amount : Math.round(amount),
life : duration,
max_life : duration
});
};
const draw_target_lock_box = (ctx, x, y, size, alpha, cell_px, color = "#f59e0b") => {
ctx.save();
ctx.translate(x, y);
ctx.globalAlpha = Math.max(0, Math.min(1.0, alpha));
const half = size / 2;
const corner_len = Math.max(cell_px * 0.35, size * 0.22);
ctx.lineWidth = cell_px * 0.14;
ctx.strokeStyle = "rgba(2, 6, 23, 0.9)";
ctx.lineCap = "square";
const draw_corners = () => {
ctx.beginPath();
ctx.moveTo(-half, -half + corner_len);
ctx.lineTo(-half, -half);
ctx.lineTo(-half + corner_len, -half);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(half - corner_len, -half);
ctx.lineTo(half, -half);
ctx.lineTo(half, -half + corner_len);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(half, half - corner_len);
ctx.lineTo(half, half);
ctx.lineTo(half - corner_len, half);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-half + corner_len, half);
ctx.lineTo(-half, half);
ctx.lineTo(-half, half - corner_len);
ctx.stroke();
};
draw_corners();
ctx.lineWidth = cell_px * 0.08;
ctx.strokeStyle = color;
draw_corners();
const cross = cell_px * 0.22;
ctx.beginPath();
ctx.moveTo(-cross, 0); ctx.lineTo(cross, 0);
ctx.moveTo(0, -cross); ctx.lineTo(0, cross);
ctx.stroke();
ctx.restore();
};
const draw_enemy_health_bar = (ctx, px, py, radius_px, health, max_health, cell_px) => {
if(health >= max_health || health <= 0) return;
const bar_w = cell_px * xgravity.settings.get("enemy_health_bar_width", null, 1.8);
const bar_h = Math.max(3, cell_px * 0.16);
const bar_x = px - (bar_w / 2);
const bar_y = py - radius_px - (cell_px * 0.45);
const pct = Math.max(0, Math.min(1.0, health / max_health));
ctx.save();
ctx.fillStyle = "rgba(2, 6, 23, 0.85)";
ctx.fillRect(bar_x - 1, bar_y - 1, bar_w + 2, bar_h + 2);
ctx.lineWidth = 1;
ctx.strokeStyle = "rgba(148, 163, 184, 0.4)";
ctx.strokeRect(bar_x - 1, bar_y - 1, bar_w + 2, bar_h + 2);
let fill_color = "#38bdf8";
if(pct <= 0.35){
fill_color = "#ef4444";
} else if(pct <= 0.65){
fill_color = "#f59e0b";
}
ctx.fillStyle = fill_color;
ctx.fillRect(bar_x, bar_y, bar_w * pct, bar_h);
ctx.restore();
};
this.update = (delta_time, drone_model, on_enemy_destroyed, on_drone_hit, on_enemy_deflected, on_mothership_impact) => {
game_clock += delta_time;
process_spawner(delta_time);
// 1. Textos flotantes
for(let i = damage_popups.length - 1; i >= 0; i--){
const pop = damage_popups[i];
pop.life -= delta_time;
if(pop.life <= 0){
damage_popups.splice(i, 1);
continue;
}
pop.x += pop.vx * delta_time;
pop.y += pop.vy * delta_time;
pop.vy *= 0.94;
}
// 2. Colisiones y dinámicas
const drone_info = drone_model ? drone_model.get_position() : null;
if(!drone_info) return;
const drone_shield_active = drone_model.is_shield_active();
const drone_radius = drone_shield_active
? xgravity.settings.get("drone_shield_radius", null, 1.7)
: (xgravity.settings.get("drone_size", null, 2.5) * 0.4);
const drone_mass = typeof drone_model.get_mass === "function"
? drone_model.get_mass()
: xgravity.settings.get("drone_mass", null, 3.0);
const restitution = xgravity.settings.get("drone_collision_restitution", null, 0.75);
const dmg_factor = xgravity.settings.get("collision_damage_factor", null, 12.0);
// ----------------------------------------------------
// ALARMA TÁCTICA MODULADA POR RIESGO
// ----------------------------------------------------
const current_threat = calculate_asteroid_threat_level();
if(current_threat > 0){
threat_beep_timer -= delta_time;
if(threat_beep_timer <= 0){
if(xgravity.audio){
// Se reproduce el beep pasando el nivel de amenaza actual
xgravity.audio.play("asteroid_threat", 1.0, current_threat);
}
// A menor riesgo (0.25) -> Pitido lento cada ~1.85 segundos
// A mayor riesgo (0.70) -> Pitido tenso cada ~0.75 segundos
// A peligro crítico (1.00) -> Pitido vertiginoso cada ~0.35 segundos
const interval = 2.2 - (current_threat * 1.85);
threat_beep_timer = Math.max(0.35, interval);
}
} else {
threat_beep_timer = 0; // Se resetea el reloj al desaparecer el peligro
}
for(let i = active_enemies.length - 1; i >= 0; i--){
const enemy = active_enemies[i];
// 1. UN SOLO UPDATE POR ENEMIGO, pasando la lista completa
enemy.update(delta_time, drone_info, active_enemies);
// 2. Comprobar si ha impactado contra la nodriza (vivo o recién muerto)
// Si el asteroide impacta la cabina:
const did_impact = enemy.impacted_mothership ||
(typeof enemy.has_impacted_mothership === "function" && enemy.has_impacted_mothership());
if(did_impact){
// Genera la gran nebulosa en el espacio exterior
spawn_mothership_nebula_explosion();
if(typeof on_mothership_impact === "function"){
const dmg = (enemy.config && enemy.config.mothership_damage) ? enemy.config.mothership_damage : 25;
on_mothership_impact(dmg);
}
active_enemies.splice(i, 1);
continue;
}
// 3. Si murió por daño u otra causa, se elimina
if(!enemy.is_alive()){
active_enemies.splice(i, 1);
continue;
}
// 4. Si está en sobrevuelo o en trayectoria a la nodriza en Z, no choca con el dron
if(!enemy.is_ready()) continue;
// 5. A partir de aquí: cálculo de colisión física con el Dron
const bounds = enemy.get_bounds();
const dx = bounds.x - drone_info.cell_x;
const dy = bounds.y - drone_info.cell_y;
const dist = Math.sqrt(dx * dx + dy * dy);
const min_dist = drone_radius + bounds.radius;
if(dist < min_dist && dist > 0.0001){
const nx = dx / dist;
const ny = dy / dist;
const enemy_vel = typeof enemy.get_velocity === "function"
? enemy.get_velocity()
: { vx: 0, vy: 0 };
const dvx = drone_info.vel_cell_x - enemy_vel.vx;
const dvy = drone_info.vel_cell_y - enemy_vel.vy;
const rel_vel = (dvx * nx) + (dvy * ny);
if(rel_vel > 0){
const enemy_mass = typeof enemy.get_mass === "function"
? enemy.get_mass()
: 2.0;
const impulse = (-(1 + restitution) * rel_vel) / ((1 / drone_mass) + (1 / enemy_mass));
const impact_intensity = Math.max(0.5, rel_vel);
const preset = xgravity.settings.get_difficulty_preset();
const drone_dmg_mult = preset ? preset.drone_damage_taken_mult : 1.0;
const enemy_dmg_mult = preset ? preset.enemy_damage_taken_mult : 1.0;
const base_dmg_factor = xgravity.settings.get("base_collision_damage_factor", null, 12.0);
const raw_damage = impact_intensity * base_dmg_factor;
const damage_to_enemy = raw_damage * enemy_dmg_mult;
const damage_to_drone = raw_damage * drone_dmg_mult;
const impact_x = drone_info.cell_x + (nx * drone_radius);
const impact_y = drone_info.cell_y + (ny * drone_radius);
spawn_damage_popup(impact_x, impact_y, damage_to_enemy);
if(typeof drone_model.apply_impulse === "function"){
drone_model.apply_impulse((impulse / drone_mass) * nx, (impulse / drone_mass) * ny);
}
if(typeof enemy.apply_impulse === "function"){
enemy.apply_impulse(-(impulse / enemy_mass) * nx, -(impulse / enemy_mass) * ny);
}
const overlap = min_dist - dist;
if(typeof drone_model.set_cell_position === "function"){
drone_model.set_cell_position(
drone_info.cell_x - (nx * overlap * 0.5),
drone_info.cell_y - (ny * overlap * 0.5)
);
}
if(typeof enemy.set_position === "function"){
enemy.set_position(
bounds.x + (nx * overlap * 0.5),
bounds.y + (ny * overlap * 0.5)
);
}
const enemy_alive = enemy.take_damage(damage_to_enemy);
if(!enemy_alive){
active_enemies.splice(i, 1);
if(drone_shield_active){
// if(typeof on_enemy_destroyed === "function") on_enemy_destroyed(enemy);
if(typeof on_enemy_destroyed === "function") on_enemy_destroyed(enemy, damage_to_enemy);
} else {
if(typeof on_drone_hit === "function") on_drone_hit(enemy, damage_to_drone);
}
} else {
if(typeof on_enemy_deflected === "function"){
on_enemy_deflected(impact_x, impact_y, nx, ny, impact_intensity, damage_to_drone, drone_shield_active);
}
}
}
}
}
// Actualizar expansión y desvanecimiento de nebulosas
for(let i = mothership_explosions.length - 1; i >= 0; i--){
const neb = mothership_explosions[i];
neb.size += neb.growth_speed * delta_time;
neb.angle += neb.rot_speed * delta_time;
// Progreso de 50 a 80 celdas (0.0 -> 1.0)
const progress = Math.min(1.0, Math.max(0, (neb.size - 50.0) / (neb.target_size - 50.0)));
neb.alpha = Math.max(0, 1.0 - progress);
if(neb.size >= neb.target_size || neb.alpha <= 0){
mothership_explosions.splice(i, 1);
}
}
};
// Método auxiliar para dibujar un sprite de enemigo
const draw_enemy_sprite = (ctx, state, cell_px, cx, cy) => {
const px = cx + (state.x * cell_px);
const py = cy + (state.y * cell_px);
const size_px = state.size * state.scale * cell_px;
ctx.save();
ctx.translate(px, py);
ctx.rotate(state.angle);
ctx.globalAlpha = Math.max(0, Math.min(1.0, state.alpha));
const img = get_cached_image(state.image_src);
if(img && img.complete && img.naturalWidth > 0){
ctx.drawImage(img, -size_px / 2, -size_px / 2, size_px, size_px);
} else {
ctx.fillStyle = state.fallback_color;
ctx.beginPath();
ctx.arc(0, 0, size_px * 0.4, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
};
// 1. CAPA INFERIOR: Enemigos en el plano de batalla (ready === true y no van a la nodriza) + Barras de vida
this.render_under = (ctx, width, height) => {
const cell_px = get_cell_size(width, height);
const cx = width / 2;
const cy = height / 2;
// Dibujar sprites que están en el plano normal de combate
for(let i = 0; i < active_enemies.length; i++){
const enemy = active_enemies[i];
const state = enemy.get_state();
// Si está muerto, aún no está listo, o es un asteroide volando hacia la nodriza en Z -> NO va aquí
const is_heading_to_mothership = typeof enemy.is_targeted_towards_mothership === "function"
&& enemy.is_targeted_towards_mothership();
if(!state.alive || !state.ready || is_heading_to_mothership) continue;
draw_enemy_sprite(ctx, state, cell_px, cx, cy);
}
// Barras de salud (solo para enemigos en combate en el plano inferior)
const show_hp = xgravity.settings.get("enemy_show_health_bars", null, true);
if(show_hp){
for(let i = 0; i < active_enemies.length; i++){
const enemy = active_enemies[i];
const state = enemy.get_state();
const is_heading_to_mothership = typeof enemy.is_targeted_towards_mothership === "function"
&& enemy.is_targeted_towards_mothership();
if(state.alive && state.ready && !is_heading_to_mothership && state.health < state.max_health){
const px = cx + (state.x * cell_px);
const py = cy + (state.y * cell_px);
const r_px = state.radius * cell_px;
draw_enemy_health_bar(ctx, px, py, r_px, state.health, state.max_health, cell_px);
}
}
}
};
// 2. CAPA SUPERIOR: Ordenada en Z (menor escala al fondo, mayor escala al frente)
this.render_over = (ctx, width, height) => {
const cell_px = get_cell_size(width, height);
const cx = width / 2;
const cy = height / 2;
// ----------------------------------------------------
// 1. CLASIFICACIÓN DE ENTIDADES
// ----------------------------------------------------
const spawning_ships = [];
const camera_asteroids = [];
for(let i = 0; i < active_enemies.length; i++){
const enemy = active_enemies[i];
const state = enemy.get_state();
if(!state.alive) continue;
const is_heading_to_mothership = typeof enemy.is_targeted_towards_mothership === "function"
&& enemy.is_targeted_towards_mothership();
if(!state.ready && !is_heading_to_mothership){
// Naves en transición de spawn hacia el plano de juego
spawning_ships.push(state);
} else if(is_heading_to_mothership){
// Asteroides proyectados en el eje Z hacia la cámara
camera_asteroids.push(state);
}
}
// ----------------------------------------------------
// 2. ORDEN INVERSO EN Z PARA ASTEROIDES
// Menor escala (más lejos) se dibuja primero;
// Mayor escala (más cerca) se dibuja después tapando al fondo.
// ----------------------------------------------------
camera_asteroids.sort((a, b) => (a.scale || 1.0) - (b.scale || 1.0));
// ----------------------------------------------------
// CAPA 1: Naves que van al mapa en orden de aparición
// ----------------------------------------------------
for(let i = 0; i < spawning_ships.length; i++){
draw_enemy_sprite(ctx, spawning_ships[i], cell_px, cx, cy);
}
// ----------------------------------------------------
// CAPA 2: Asteroides que van a cámara en orden inverso
// ----------------------------------------------------
for(let i = 0; i < camera_asteroids.length; i++){
draw_enemy_sprite(ctx, camera_asteroids[i], cell_px, cx, cy);
}
// ----------------------------------------------------
// CAPA 3: Nebulosas de explosión contra la nave nodriza
// ----------------------------------------------------
for(let i = 0; i < mothership_explosions.length; i++){
const neb = mothership_explosions[i];
if(neb.image && neb.image.complete && neb.image.naturalWidth !== 0 && neb.alpha > 0){
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(neb.angle);
ctx.globalAlpha = neb.alpha;
const size_px = neb.size * cell_px;
ctx.drawImage(neb.image, -size_px / 2, -size_px / 2, size_px, size_px);
ctx.restore();
}
}
// ----------------------------------------------------
// RETÍCULAS Y DAÑO FLOTANTE (Sobre las entidades)
// ----------------------------------------------------
const lock_duration = xgravity.settings.get("enemy_target_lock_duration", null, 1.2);
for(let i = 0; i < active_enemies.length; i++){
const enemy = active_enemies[i];
const is_heading_to_mothership = typeof enemy.is_targeted_towards_mothership === "function"
&& enemy.is_targeted_towards_mothership();
if(is_heading_to_mothership) continue;
const age = typeof enemy.get_age === "function" ? enemy.get_age() : 0;
if(age < lock_duration){
const bounds = enemy.get_bounds();
const px = cx + (bounds.x * cell_px);
const py = cy + (bounds.y * cell_px);
const box_size = Math.max(cell_px * 1.6, (bounds.radius * 2 * cell_px) * 1.35);
const remaining_ratio = 1.0 - (age / lock_duration);
const alpha = Math.min(1.0, remaining_ratio * 1.5);
draw_target_lock_box(ctx, px, py, box_size, alpha, cell_px, "#f59e0b");
}
}
if(damage_popups.length > 0){
ctx.save();
ctx.font = `bold ${Math.round(cell_px * 0.55)}px "Courier New", monospace`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
for(let i = 0; i < damage_popups.length; i++){
const pop = damage_popups[i];
const px = cx + (pop.x * cell_px);
const py = cy + (pop.y * cell_px);
const alpha = Math.max(0, pop.life / pop.max_life);
ctx.globalAlpha = alpha;
ctx.fillStyle = "rgba(0, 0, 0, 0.85)";
ctx.fillText(`-${pop.amount}`, px + 1.5, py + 1.5);
ctx.fillStyle = "#fbbf24";
ctx.fillText(`-${pop.amount}`, px, py);
}
ctx.restore();
}
};
// Fallback de conveniencia por si se llamara al método global
this.render = (ctx, width, height) => {
self.render_under(ctx, width, height);
self.render_over(ctx, width, height);
};
};
return EnemiesManager;
})();

View File

@ -0,0 +1,70 @@
"use strict";
const ExplosionsManager = (function(){
const ExplosionsManager = function(xgravity){
const self = this;
const shield_fx = new ShieldExplosionsFx(xgravity);
const hull_fx = new HullHitExplosionsFx(xgravity);
const sparks_fx = new DeflectionSparksFx(xgravity);
const cockpit_fx = new CockpitImpactFx(xgravity);
const get_cell_size = (width, height) => {
const cells = xgravity.settings.get("cells", null, 40);
return (width < height ? width : height) / cells;
};
this.add = (cell_x, cell_y, type = "shield") => {
if(type === "shield"){
shield_fx.add(cell_x, cell_y);
} else {
hull_fx.add(cell_x, cell_y);
}
};
this.add_sparks = (cell_x, cell_y, normal_x, normal_y, intensity) => {
sparks_fx.add(cell_x, cell_y, normal_x, normal_y, intensity);
};
this.add_mothership_impact = () => {
cockpit_fx.trigger();
};
this.update = (delta_time) => {
shield_fx.update(delta_time);
hull_fx.update(delta_time);
sparks_fx.update(delta_time);
cockpit_fx.update(delta_time);
};
// Renderizado del plano de batalla (ondas y chispas espaciales)
this.render = (ctx, width, height) => {
const cell_px = get_cell_size(width, height);
const cx = width / 2;
const cy = height / 2;
ctx.save();
ctx.globalCompositeOperation = "lighter";
shield_fx.render(ctx, cell_px, cx, cy);
hull_fx.render(ctx, cell_px, cx, cy);
sparks_fx.render(ctx, cell_px, cx, cy);
ctx.restore();
};
// Renderizado exclusivo de la cabina (pantalla completa y rocas sólidas)
this.render_cockpit = (ctx, width, height) => {
cockpit_fx.render(ctx, width, height);
};
this.reset = () => {
shield_fx.reset();
hull_fx.reset();
sparks_fx.reset();
cockpit_fx.reset();
};
};
return ExplosionsManager;
})();

View File

@ -0,0 +1,81 @@
"use strict";
const I18NManager = (function(){
const I18NManager = function(xgravity){
const self = this,
sentences = {};
let language = "espanol";
const constructor = () => {
[i18n, typeof custom_i18n != "undefined" ? custom_i18n : {}].forEach(block => {
self.add(block, true);
});
self.set_language(xgravity.settings.get("language"), null, Object.keys(sentences)[0] || language);
};
this.add = (new_sentences, overwrite = true) => {
for(const language in new_sentences){
if(!sentences[language])
sentences[language] = {};
for(const key in new_sentences[language])
if(overwrite || sentences[language][key] === undefined)
sentences[language][key] = new_sentences[language][key];
};
};
const get_sentence = (strings, _default) => {
const done = [];
if(typeof strings == "string")
strings = [strings];
for(const current of [language].concat(Object.keys(sentences)))
if(!done.includes(current)){
done.push(current);
for(const key of strings)
if(sentences[current][key] !== undefined)
return sentences[current][key];
};
if(_default !== null)
return _default;
for(const text of strings)
return text;
return "";
};
this.get = (strings, inputs = null, _default = null) => Common.string_variables(get_sentence(strings, _default), inputs);
this.update = () => {
document.querySelectorAll("[data-i18n]").forEach(element => {
const text = self.get(element.getAttribute("data-i18n"));
if(element.getAttribute("data-i18n-without") != "true")
element.textContent = text;
["title", "alt", "placeholder"].forEach(key => {
element.hasAttribute(key) && element.setAttribute(key, text);
});
})
};
this.set_language = new_language => {
if(language != new_language && sentences[new_language]){
language = new_language;
self.update();
};
};
this.get_languages = () => Object.keys(sentences);
this.get_current_language = () => language;
constructor();
};
return I18NManager;
})();

View File

@ -0,0 +1,40 @@
"use strict";
const SettingsManager = (function(){
const SettingsManager = function(xgravity){
const self = this,
data = {};
const constructor = () => {
self.add(settings, true);
};
this.add = (block, overwrite = true) => {
for(const key in block)
if(overwrite || data[key] === undefined)
data[key] = block[key];
};
this.get = (keys, inputs = null, _default = null) => Common.get_value(keys, [inputs, data], _default);
this.get_difficulty_preset = () => {
const diff_key = self.get("current_difficulty", null, "average");
const presets = self.get("difficulty_presets", null, {});
return presets[diff_key] || presets["average"];
};
this.get_effective_stat = (base_key, mult_key) => {
const base_val = self.get(base_key, null, 1.0);
const preset = self.get_difficulty_preset();
const mult = (preset && typeof preset[mult_key] === "number") ? preset[mult_key] : 1.0;
return base_val * mult;
};
constructor();
};
return SettingsManager;
})();

View File

@ -0,0 +1,174 @@
"use strict";
const AsteroidEnemyModel = (function(){
const AsteroidEnemyModel = function(xgravity, config = {}){
const self = this;
const resolve_image = (img) => {
if(Array.isArray(img) && img.length > 0){
return img[Math.floor(Math.random() * img.length)];
}
return typeof img === "string" ? img : null;
};
const image_src = resolve_image(config.image);
const base_size = config.size || 2.4;
const points = config.points || 80;
const mass = config.mass || 7.0;
let health = config.health || 50;
const max_health = health;
let towards_mothership = false;
const rotation_speed = (Math.random() - 0.5) * 1.5;
const entity = {
x : 0,
y : 0,
vx : 0,
vy : 0,
angle : Math.random() * Math.PI * 2,
scale : 1.0,
alpha : 1.0,
ready : false,
alive : true,
age : 0,
config : config
};
const spawn_strategy = SpawnStrategies.get(config.spawn_mode || "perimeter");
const behavior_strategy = BehaviorStrategies.get(config.behavior || "cross_screen");
this.is_asteroid = true;
this.is_targeted_towards_mothership = () => towards_mothership;
this.has_impacted_mothership = () => Boolean(entity.impacted_mothership);
// Lanzamiento hacia el centro
this.launch_towards_mothership = (lvx, lvy) => {
entity.vx = lvx;
entity.vy = lvy;
towards_mothership = true;
entity.alpha = 1.0; // Garantiza 100% de opacidad sin transparencia de spawn
};
// Si vuela hacia la nodriza, queremos que se pinte en render_over por encima del plano de combate
// Cuando towards_mothership = true, se fuerza a FALSE para que el gestor lo pinte en render_over
this.is_ready = () => entity.ready && !towards_mothership;
this.is_alive = () => entity.alive;
this.get_points = () => points;
this.get_age = () => entity.age;
this.get_mass = () => mass;
this.get_health = () => health;
this.get_max_health = () => max_health;
this.get_velocity = () => ({ vx: entity.vx, vy: entity.vy });
this.get_state = () => ({
x : entity.x,
y : entity.y,
angle : entity.angle,
scale : entity.scale,
alpha : 1.0, // Siempre nítido en el eje Z
size : base_size,
image_src : image_src,
ready : entity.ready && !towards_mothership, // En render_over cuando vuela al cristal
alive : entity.alive,
health : health,
max_health : max_health,
radius : (base_size * entity.scale) * 0.45,
fallback_color : towards_mothership ? "#ef4444" : "#94a3b8"
});
this.get_bounds = () => ({
x : entity.x,
y : entity.y,
radius : (base_size * entity.scale) * 0.45
});
this.apply_impulse = (ivx, ivy) => {
entity.vx += ivx;
entity.vy += ivy;
// Si el dron lo choca con escudo, desvía la trayectoria y frena el crecimiento
towards_mothership = false;
entity.scale = Math.min(entity.scale, 2.5);
};
this.set_velocity = (nvx, nvy) => {
entity.vx = nvx;
entity.vy = nvy;
};
this.set_position = (nx, ny) => {
entity.x = nx;
entity.y = ny;
};
this.take_damage = (amount) => {
health = Math.max(0, health - amount);
if(health <= 0){
entity.alive = false;
}
return entity.alive;
};
this.on_hit = () => {
entity.alive = false;
};
this.update = (delta_time, drone_info, all_enemies) => {
if(!entity.alive) return;
entity.age += delta_time;
if(!entity.ready){
spawn_strategy.update(entity, delta_time);
} else if(towards_mothership){
// 1. VIAJE HACIA EL CRISTAL DE LA CABINA (EJE Z)
entity.x += entity.vx * delta_time;
entity.y += entity.vy * delta_time;
// Atracción suave hacia el centro absoluto (0, 0)
const dist_center = Math.sqrt(entity.x * entity.x + entity.y * entity.y);
if(dist_center > 0.1){
const pull_speed = 6.0;
entity.vx = -(entity.x / dist_center) * pull_speed;
entity.vy = -(entity.y / dist_center) * pull_speed;
}
// Crecimiento de escala hasta llegar a un tamaño de 40 celdas
// Tamaño actual = base_size * entity.scale
const target_cells = 40.0;
const scale_target = target_cells / base_size; // ~16.66
const growth_speed = 10.5; // Rapidez de expansión visual hacia la cámara
entity.scale += growth_speed * delta_time;
// Al alcanzar el tamaño gigante, se estrella en la pantalla
if((base_size * entity.scale) >= target_cells){
entity.alive = false;
entity.impacted_mothership = true;
self.impacted_mothership = true; // <-- Asignado también directamente a la instancia
}
} else {
// Vuelo normal
behavior_strategy(entity, delta_time, drone_info, all_enemies);
// Si se sale del marco de juego (> 24.5 celdas) se elimina definitivamente
const dist_center = Math.sqrt(entity.x * entity.x + entity.y * entity.y);
if(dist_center > 24.5 && entity.age > 1.2){
entity.alive = false;
}
}
// Giro sobre su eje
entity.angle += rotation_speed * delta_time;
};
const constructor = () => {
spawn_strategy.init(entity, config, xgravity);
};
constructor();
};
return AsteroidEnemyModel;
})();

View File

@ -0,0 +1,112 @@
"use strict";
const AsteroidThrowerEnemyModel = (function(){
const AsteroidThrowerEnemyModel = function(xgravity, config = {}){
const self = this;
const resolve_image = (img) => {
if(Array.isArray(img) && img.length > 0){
return img[Math.floor(Math.random() * img.length)];
}
return typeof img === "string" ? img : null;
};
const image_src = resolve_image(config.image);
const base_size = config.size || 2.0;
const points = config.points || 250;
const mass = config.mass || 2.2;
let health = config.health || 45;
const max_health = health;
const entity = {
x : 0,
y : 0,
vx : 0,
vy : 0,
angle : 0,
scale : 1.0,
alpha : 1.0,
ready : false,
alive : true,
age : 0,
config : config
};
const spawn_strategy = SpawnStrategies.get(config.spawn_mode || "perimeter");
const behavior_strategy = BehaviorStrategies.get(config.behavior || "thrower_ai");
this.is_ready = () => entity.ready;
this.is_alive = () => entity.alive;
this.get_points = () => points;
this.get_age = () => entity.age;
this.get_mass = () => mass;
this.get_health = () => health;
this.get_max_health = () => max_health;
this.get_velocity = () => ({ vx: entity.vx, vy: entity.vy });
this.get_state = () => ({
x : entity.x,
y : entity.y,
angle : entity.angle,
scale : entity.scale,
alpha : entity.alpha,
size : base_size,
image_src : image_src,
ready : entity.ready,
alive : entity.alive,
health : health,
max_health : max_health,
radius : (base_size * entity.scale) * 0.45,
fallback_color : entity.ready ? "#eab308" : "#fef08a"
});
this.get_bounds = () => ({
x : entity.x,
y : entity.y,
radius : (base_size * entity.scale) * 0.45
});
this.apply_impulse = (ivx, ivy) => {
entity.vx += ivx;
entity.vy += ivy;
};
this.set_position = (nx, ny) => {
entity.x = nx;
entity.y = ny;
};
this.take_damage = (amount) => {
health = Math.max(0, health - amount);
if(health <= 0){
entity.alive = false;
}
return entity.alive;
};
this.on_hit = () => {
entity.alive = false;
};
this.update = (delta_time, drone_info, all_enemies) => {
if(!entity.alive) return;
entity.age += delta_time;
if(!entity.ready){
spawn_strategy.update(entity, delta_time);
} else {
behavior_strategy(entity, delta_time, drone_info, all_enemies);
}
};
const constructor = () => {
spawn_strategy.init(entity, config, xgravity);
};
constructor();
};
return AsteroidThrowerEnemyModel;
})();

View File

@ -0,0 +1,105 @@
"use strict";
const BouncerEnemyModel = (function(){
const BouncerEnemyModel = function(xgravity, config = {}){
const self = this;
const entity = {
x : 0,
y : 0,
vx : 0,
vy : 0,
angle : 0,
scale : 1.0,
alpha : 1.0,
ready : false,
alive : true,
age : 0,
config : config
};
const base_size = config.size || 2.2;
const points = config.points || 100;
const mass = config.mass || 2.5;
let health = config.health || 60;
const max_health = health;
const image_src = config.image || null;
const spawn_strategy = SpawnStrategies.get(config.spawn_mode || "perimeter");
const behavior_strategy = BehaviorStrategies.get(config.behavior || "bounce_screen");
this.is_ready = () => entity.ready;
this.is_alive = () => entity.alive;
this.get_points = () => points;
this.get_age = () => entity.age;
this.get_mass = () => mass;
this.get_health = () => health;
this.get_max_health = () => max_health;
this.get_velocity = () => ({ vx: entity.vx, vy: entity.vy });
this.get_state = () => ({
x : entity.x,
y : entity.y,
angle : entity.angle,
scale : entity.scale,
alpha : entity.alpha,
size : base_size,
image_src : image_src,
ready : entity.ready,
alive : entity.alive,
health : health,
max_health : max_health,
radius : (base_size * entity.scale) * 0.45,
fallback_color : entity.ready ? "#10b981" : "#34d399"
});
this.get_bounds = () => ({
x : entity.x,
y : entity.y,
radius : (base_size * entity.scale) * 0.45
});
this.apply_impulse = (ivx, ivy) => {
entity.vx += ivx;
entity.vy += ivy;
};
this.set_position = (nx, ny) => {
entity.x = nx;
entity.y = ny;
};
this.take_damage = (amount) => {
health = Math.max(0, health - amount);
if(health <= 0){
entity.alive = false;
}
return entity.alive;
};
this.on_hit = () => {
entity.alive = false;
};
this.update = (delta_time, drone_info) => {
if(!entity.alive) return;
entity.age += delta_time;
if(!entity.ready){
spawn_strategy.update(entity, delta_time);
} else {
behavior_strategy(entity, delta_time, drone_info);
}
};
const constructor = () => {
spawn_strategy.init(entity, config, xgravity);
};
constructor();
};
return BouncerEnemyModel;
})();

View File

@ -0,0 +1,111 @@
"use strict";
const ChaserEnemyModel = (function(){
const ChaserEnemyModel = function(xgravity, config = {}){
const self = this;
// Objeto de contexto mutable que manipulan las estrategias
const entity = {
x : 0,
y : 0,
vx : 0,
vy : 0,
angle : 0,
scale : 1.0,
alpha : 1.0,
ready : false,
alive : true,
age : 0,
config : config
};
const base_size = config.size || 1.8;
const points = config.points || 150;
const mass = config.mass || 1.8;
let health = config.health || 30;
const max_health = health;
const image_src = config.image || null;
// Enlace con las estrategias según config
const spawn_strategy = SpawnStrategies.get(config.spawn_mode || "overflight");
const behavior_strategy = BehaviorStrategies.get(config.behavior || "track_drone");
// Accesores de estado para EnemiesManager y físicas
this.is_ready = () => entity.ready;
this.is_alive = () => entity.alive;
this.get_points = () => points;
this.get_age = () => entity.age;
this.get_mass = () => mass;
this.get_health = () => health;
this.get_max_health = () => max_health;
this.get_velocity = () => ({ vx: entity.vx, vy: entity.vy });
this.get_state = () => ({
x : entity.x,
y : entity.y,
angle : entity.angle,
scale : entity.scale,
alpha : entity.alpha,
size : base_size,
image_src : image_src,
ready : entity.ready,
alive : entity.alive,
health : health,
max_health : max_health,
radius : (base_size * entity.scale) * 0.45,
fallback_color : entity.ready ? "#a855f7" : "#c084fc"
});
this.get_bounds = () => ({
x : entity.x,
y : entity.y,
radius : (base_size * entity.scale) * 0.45
});
this.apply_impulse = (ivx, ivy) => {
entity.vx += ivx;
entity.vy += ivy;
};
this.set_position = (nx, ny) => {
entity.x = nx;
entity.y = ny;
};
this.take_damage = (amount) => {
health = Math.max(0, health - amount);
if(health <= 0){
entity.alive = false;
}
return entity.alive;
};
this.on_hit = () => {
entity.alive = false;
};
this.update = (delta_time, drone_info) => {
if(!entity.alive) return;
entity.age += delta_time;
if(!entity.ready){
// Ejecuta la cinemática de spawn hasta que marque ready = true
spawn_strategy.update(entity, delta_time);
} else {
// Ejecuta la IA de combate
behavior_strategy(entity, delta_time, drone_info);
}
};
const constructor = () => {
// Inicializa la posición y cinemática de entrada según la estrategia configurada
spawn_strategy.init(entity, config, xgravity);
};
constructor();
};
return ChaserEnemyModel;
})();

View File

@ -0,0 +1,162 @@
"use strict";
const CockpitModel = (function(){
const CockpitModel = function(xgravity){
const self = this;
let blink_timer = 0;
let alert_state = false;
const max_hull = xgravity.settings.get("base_mothership_hull_max", null, 100.0);
let current_hull = max_hull;
this.get_health_percentage = () => Math.max(0, current_hull / max_hull);
this.get_hull = () => current_hull;
this.get_max_hull = () => max_hull;
this.take_damage = (amount) => {
current_hull = Math.max(0, current_hull - amount);
self.set_alert(true);
return current_hull <= 0; // true si explota la cabina (Game Over)
};
this.repair = (amount) => {
current_hull = Math.min(max_hull, current_hull + amount);
};
this.update = (delta_time) => {
// Autoreparación progresiva regulada por dificultad
const base_repair = xgravity.settings.get("base_mothership_repair_rate", null, 0.75);
const preset = xgravity.settings.get_difficulty_preset();
const repair_mult = preset ? preset.mothership_repair_mult : 1.0;
if(current_hull < max_hull && current_hull > 0){
current_hull = Math.min(max_hull, current_hull + (base_repair * repair_mult * delta_time));
}
};
const get_cell_size = (width, height) => {
const cells = xgravity.settings.get("cells", null, 40);
return (width < height ? width : height) / cells;
};
this.set_alert = (state) => {
alert_state = Boolean(state);
};
this.get_alert = () => {
return alert_state;
};
this.render = (ctx, width, height, delta_time) => {
const cell_px = get_cell_size(width, height);
const cx = width / 2;
const cy = height / 2;
const min_dim = Math.min(width, height);
const max_dim = Math.max(width, height);
blink_timer += delta_time;
// Escotilla panorámica ampliada: desborda la dimensión menor
// para que el espacio exterior sea el protagonista absoluto.
const inner_radius = (min_dim / 2) * 1.18;
const outer_radius = max_dim * 0.85;
const frame_thickness = cell_px * 0.6;
ctx.save();
// 1. Efecto sutil de cristal panorámico en el área de combate
const glass_grad = ctx.createLinearGradient(cx - inner_radius, cy - inner_radius, cx + inner_radius, cy + inner_radius);
glass_grad.addColorStop(0, "rgba(56, 189, 248, 0.04)");
glass_grad.addColorStop(0.5, "rgba(255, 255, 255, 0.01)");
glass_grad.addColorStop(1, "rgba(2, 132, 199, 0.03)");
ctx.fillStyle = glass_grad;
ctx.beginPath();
ctx.arc(cx, cy, inner_radius, 0, Math.PI * 2);
ctx.fill();
// 2. Anillo de la cúpula (Marco perimetral reforzado)
ctx.lineWidth = frame_thickness;
ctx.strokeStyle = "#0b1120";
ctx.beginPath();
ctx.arc(cx, cy, inner_radius + (frame_thickness / 2), 0, Math.PI * 2);
ctx.stroke();
// Bisel fino interior iluminado
ctx.lineWidth = cell_px * 0.06;
ctx.strokeStyle = "rgba(56, 189, 248, 0.3)";
ctx.beginPath();
ctx.arc(cx, cy, inner_radius, 0, Math.PI * 2);
ctx.stroke();
// 3. Montantes / Vigas radiales exteriores (8 vigas perimetrales)
const SECTORS = 8;
const beam_width = frame_thickness * 0.75;
for(let i = 0; i < SECTORS; i++){
const angle = (i * Math.PI * 2) / SECTORS + (Math.PI / 8);
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const start_x = cx + cos * inner_radius;
const start_y = cy + sin * inner_radius;
const end_x = cx + cos * outer_radius;
const end_y = cy + sin * outer_radius;
// Viga estructural
ctx.lineWidth = beam_width;
ctx.strokeStyle = "#090d16";
ctx.beginPath();
ctx.moveTo(start_x, start_y);
ctx.lineTo(end_x, end_y);
ctx.stroke();
// Detalle central de viga
ctx.lineWidth = beam_width * 0.2;
ctx.strokeStyle = "#1e293b";
ctx.beginPath();
ctx.moveTo(start_x, start_y);
ctx.lineTo(end_x, end_y);
ctx.stroke();
// 4. Sensores / Remaches LED en la unión del anillo
const light_dist = inner_radius + (frame_thickness * 0.5);
const lx = cx + cos * light_dist;
const ly = cy + sin * light_dist;
const light_radius = cell_px * 0.12;
const is_lit = alert_state ? (Math.sin(blink_timer * 14) > 0) : ((Math.sin(blink_timer * 2 + i) > 0.2));
const glow_color = alert_state ? "#ef4444" : (i % 2 === 0 ? "#10b981" : "#0284c7");
ctx.fillStyle = is_lit ? glow_color : "#1e293b";
ctx.beginPath();
ctx.arc(lx, ly, light_radius, 0, Math.PI * 2);
ctx.fill();
if(is_lit){
ctx.shadowColor = glow_color;
ctx.shadowBlur = cell_px * 0.4;
ctx.fillStyle = glow_color;
ctx.beginPath();
ctx.arc(lx, ly, light_radius * 0.6, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
}
// 5. Viñeteado de cabina muy sutil en los extremos de la pantalla
const vig_grad = ctx.createRadialGradient(cx, cy, inner_radius * 0.95, cx, cy, outer_radius);
vig_grad.addColorStop(0, "rgba(5, 7, 13, 0)");
vig_grad.addColorStop(0.6, "rgba(5, 7, 13, 0.25)");
vig_grad.addColorStop(1, "rgba(2, 4, 8, 0.7)");
ctx.fillStyle = vig_grad;
ctx.fillRect(0, 0, width, height);
ctx.restore();
};
};
return CockpitModel;
})();

View File

@ -0,0 +1,442 @@
"use strict";
const DroneModel = (function(){
const DroneModel = function(xgravity){
const self = this;
let image = null;
let is_loaded = false;
// Posición y velocidad en CELDAS relativas al centro
let cell_x = 0;
let cell_y = 0;
let vel_cell_x = 0;
let vel_cell_y = 0;
let angle = 0;
// Objetivo en CELDAS
let target_cell_x = 0;
let target_cell_y = 0;
let has_cursor = false;
// Animación cinemática de Despliegue / Lanzamiento
let is_deploying = true;
let deploy_timer = 0;
const deploy_duration = 1.25; // Duración en segundos de la transición
const start_scale = 10.0; // Tamaño inicial x10 emergiendo de cabina
let current_scale = start_scale;
let deploy_start_x = 0;
let deploy_start_y = 24.0; // Emerge por debajo de la pantalla
let deploy_target_x = 0;
let deploy_target_y = 0;
// Estado del Escudo
let shield = 100;
const max_shield = xgravity.settings.get_effective_stat("base_drone_shield_max", "shield_capacity_mult");
let shield_energy = max_shield;
let shield_active = false;
let shield_pulse_timer = 0;
let is_critical = false;
let beacon_angle = 0;
let alarm_audio_timer = 0;
// Estados dinámicos de alerta
let shield_risk = 0.0; // 0.0 = seguro, 1.0 = colapso inminente
let perimeter_risk = 0.0; // 0.0 = seguro, 1.0 = desintegración perimetral
let shield_alarm_timer = 0;
let perimeter_alarm_timer = 0;
this.is_deploying = () => is_deploying;
this.get_perimeter_risk = () => perimeter_risk;
this.get_shield_risk = () => shield_risk;
this.get_mass = () => xgravity.settings.get("drone_mass", null, 3.0);
this.get_shield_percentage = () => Math.max(0, shield_energy / max_shield);
this.apply_impulse = (impulse_vx, impulse_vy) => {
if(is_deploying) return;
vel_cell_x += impulse_vx;
vel_cell_y += impulse_vy;
};
this.set_cell_position = (nx, ny) => {
cell_x = nx;
cell_y = ny;
};
this.restore_shield = (amount) => {
if(amount > 0){
shield = Math.min(max_shield, shield + amount);
}
};
const get_cell_size = (width, height) => {
const cells = xgravity.settings.get("cells", null, 40);
return (width < height ? width : height) / cells;
};
this.set_shield_active = (state) => {
if(is_deploying) return;
// Solo se puede encender si le queda energía disponible
if(state && shield <= 0){
shield_active = false;
return;
}
shield_active = Boolean(state);
};
this.is_shield_active = () => !is_deploying && shield_active && shield > 0;
this.set_target_pixels = (pixel_x, pixel_y, canvas_w, canvas_h) => {
const cell_px = get_cell_size(canvas_w, canvas_h);
const center_x = canvas_w / 2;
const center_y = canvas_h / 2;
target_cell_x = (pixel_x - center_x) / cell_px;
target_cell_y = (pixel_y - center_y) / cell_px;
has_cursor = true;
};
this.start_deployment = () => {
is_deploying = true;
deploy_timer = 0;
current_scale = start_scale;
// Aterrizaje aleatorio en la zona central
deploy_target_x = (Math.random() - 0.5) * 6.0; // Entre -3 y +3 celdas
deploy_target_y = (Math.random() - 0.5) * 5.0; // Entre -2.5 y +2.5 celdas
// Punto de despegue: parte inferior de la pantalla
deploy_start_x = deploy_target_x * 0.35;
deploy_start_y = 23.5;
cell_x = deploy_start_x;
cell_y = deploy_start_y;
vel_cell_x = 0;
vel_cell_y = 0;
// Apuntar morro en la dirección de eyección (hacia arriba)
angle = Math.atan2(deploy_target_y - deploy_start_y, deploy_target_x - deploy_start_x) + (Math.PI / 2);
};
this.init_position_cells = (cx = 0, cy = 0) => {
this.start_deployment();
};
this.init_position = (x, y) => {
this.start_deployment();
};
this.get_position = () => {
return {
cell_x : cell_x,
cell_y : cell_y,
vel_cell_x : vel_cell_x,
vel_cell_y : vel_cell_y,
target_cell_x : target_cell_x,
target_cell_y : target_cell_y,
angle : angle,
shield : shield,
max_shield : max_shield,
is_shield_active : !is_deploying && shield_active && shield > 0,
is_deploying : is_deploying,
scale : current_scale
};
};
this.damage_shield = (amount) => {
if(is_deploying) return;
shield = Math.max(0, shield - amount);
if(shield <= 0) shield_active = false;
};
this.update = (delta_time) => {
// ----------------------------------------------------
// FASE 1: ANIMACIÓN DE LANZAMIENTO DESDE CABINA
// ----------------------------------------------------
if(is_deploying){
deploy_timer += delta_time;
const t = Math.min(1.0, deploy_timer / deploy_duration);
// Desaceleración suave (Cubic Ease-Out)
const ease = 1.0 - Math.pow(1.0 - t, 3);
// Interpolar posición y escala (de x10 a x1.0)
cell_x = deploy_start_x + ((deploy_target_x - deploy_start_x) * ease);
cell_y = deploy_start_y + ((deploy_target_y - deploy_start_y) * ease);
current_scale = start_scale + ((1.0 - start_scale) * ease);
angle = Math.atan2(deploy_target_y - deploy_start_y, deploy_target_x - deploy_start_x) + (Math.PI / 2);
if(t >= 1.0){
is_deploying = false;
current_scale = 1.0;
cell_x = deploy_target_x;
cell_y = deploy_target_y;
vel_cell_x = 0;
vel_cell_y = 0;
if(!has_cursor){
target_cell_x = deploy_target_x;
target_cell_y = deploy_target_y;
}
}
return; // Bloquea consumo de escudo y avisos durante el despegue
}
// ----------------------------------------------------
// FASE 2: CONTROL Y FÍSICAS DE JUEGO NORMAL
// ----------------------------------------------------
if(!has_cursor){
target_cell_x = 0;
target_cell_y = 0;
}
// Dinámica del Escudo (Consumo y Recarga)
shield_pulse_timer += delta_time;
const drain_rate = xgravity.settings.get("drone_shield_drain", null, 25);
const recharge_time = Math.max(0.1, xgravity.settings.get("drone_shield_recharge_time", null, 8.0));
const recovery_rate = max_shield / recharge_time;
const shield_pct = (shield / max_shield) * 100;
const shield_th = xgravity.settings.get("drone_shield_low_threshold", null, 30);
if(shield_active){
shield = Math.max(0, shield - (drain_rate * delta_time));
if(shield <= 0){
shield_active = false;
}
} else {
shield = Math.min(max_shield, shield + (recovery_rate * delta_time));
}
// Físicas orbitales hacia el cursor
const dcx = target_cell_x - cell_x;
const dcy = target_cell_y - cell_y;
angle = Math.atan2(dcy, dcx) + (Math.PI / 2);
const f = xgravity.settings.get("drone_force", null, 3.0);
const [new_vx, new_vy] = DroneModel.drone_velocity(
target_cell_x, target_cell_y,
cell_x, cell_y,
vel_cell_x, vel_cell_y,
f,
delta_time
);
if(shield_pct <= shield_th){
shield_risk = (shield_th - shield_pct) / shield_th;
is_critical = true;
} else {
shield_risk = 0.0;
is_critical = false;
}
// Alarma de escudo progresiva
if(shield_risk > 0.0){
const beacon_speed = 5.0 + (shield_risk * 10.0);
beacon_angle += beacon_speed * delta_time;
const max_int = xgravity.settings.get("drone_shield_alarm_interval_max", null, 0.65);
const min_int = xgravity.settings.get("drone_shield_alarm_interval_min", null, 0.18);
const current_interval = max_int - (shield_risk * (max_int - min_int));
const current_vol = 0.2 + (shield_risk * 0.8);
shield_alarm_timer += delta_time;
if(shield_alarm_timer >= current_interval){
shield_alarm_timer = 0;
if(xgravity.audio){
xgravity.audio.play("shield_low_alarm", current_vol);
}
}
} else {
shield_alarm_timer = 0;
}
// Alarma de perímetro
const dist_center = Math.sqrt(cell_x * cell_x + cell_y * cell_y);
const safe_r = xgravity.settings.get("drone_perimeter_safe_radius", null, 15.0);
const crit_r = xgravity.settings.get("drone_perimeter_critical_radius", null, 18.5);
if(dist_center > safe_r){
perimeter_risk = Math.min(1.0, (dist_center - safe_r) / (crit_r - safe_r));
} else {
perimeter_risk = 0.0;
}
if(perimeter_risk > 0.0){
const max_p_int = xgravity.settings.get("drone_perimeter_alarm_interval_max", null, 0.7);
const min_p_int = xgravity.settings.get("drone_perimeter_alarm_interval_min", null, 0.2);
const p_interval = max_p_int - (perimeter_risk * (max_p_int - min_p_int));
const p_vol = 0.2 + (perimeter_risk * 0.8);
perimeter_alarm_timer += delta_time;
if(perimeter_alarm_timer >= p_interval){
perimeter_alarm_timer = 0;
if(xgravity.audio){
xgravity.audio.play("perimeter_warning", p_vol);
}
}
} else {
perimeter_alarm_timer = 0;
}
vel_cell_x = new_vx;
vel_cell_y = new_vy;
cell_x += vel_cell_x * delta_time;
cell_y += vel_cell_y * delta_time;
};
this.render = (ctx, width, height) => {
const cell_px = get_cell_size(width, height);
const center_x = width / 2;
const center_y = height / 2;
const px = center_x + (cell_x * cell_px);
const py = center_y + (cell_y * cell_px);
// Tamaño base del dron escalado según la fase de vuelo (x10 -> x1.0)
const drone_cells = xgravity.settings.get("drone_size", null, 2.5);
const drone_size_px = drone_cells * cell_px * current_scale;
ctx.save();
ctx.translate(px, py);
// 1. Burbuja del Escudo (solo en combate normal con escudo encendido)
if(!is_deploying && shield_active && shield > 0){
const shield_radius_cells = xgravity.settings.get("drone_shield_radius", null, 1.7);
const shield_radius_px = shield_radius_cells * cell_px;
const pulse = Math.sin(shield_pulse_timer * 18) * (cell_px * 0.08);
const r = shield_radius_px + pulse;
ctx.save();
const shield_grad = ctx.createRadialGradient(0, 0, r * 0.5, 0, 0, r);
shield_grad.addColorStop(0, "rgba(56, 189, 248, 0.05)");
shield_grad.addColorStop(0.7, "rgba(56, 189, 248, 0.2)");
shield_grad.addColorStop(1, "rgba(56, 189, 248, 0.6)");
ctx.fillStyle = shield_grad;
ctx.beginPath();
ctx.arc(0, 0, r, 0, Math.PI * 2);
ctx.fill();
ctx.lineWidth = cell_px * 0.12;
ctx.strokeStyle = "rgba(186, 230, 253, 0.9)";
ctx.shadowColor = "#38bdf8";
ctx.shadowBlur = cell_px * 0.5;
ctx.beginPath();
ctx.arc(0, 0, r, 0, Math.PI * 2);
ctx.stroke();
ctx.lineWidth = cell_px * 0.06;
ctx.setLineDash([cell_px * 0.3, cell_px * 0.2]);
ctx.strokeStyle = "rgba(56, 189, 248, 0.7)";
ctx.beginPath();
ctx.arc(0, 0, r + (cell_px * 0.2), 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
// 2. Renderizado del Sprite
ctx.rotate(angle);
if(is_loaded && image){
const ratio = image.naturalWidth / image.naturalHeight;
let w = drone_size_px;
let h = drone_size_px;
if(ratio >= 1){
h = drone_size_px / ratio;
} else {
w = drone_size_px * ratio;
}
ctx.drawImage(image, -w / 2, -h / 2, w, h);
} else {
ctx.fillStyle = "#38bdf8";
ctx.beginPath();
ctx.moveTo(0, -drone_size_px / 2);
ctx.lineTo(drone_size_px / 3, drone_size_px / 2);
ctx.lineTo(-drone_size_px / 3, drone_size_px / 2);
ctx.closePath();
ctx.fill();
}
// Girofaro crítico
if(!is_deploying && is_critical){
const pulse = 0.5 + Math.sin(beacon_angle * 2) * 0.5;
const halo_radius = cell_px * (2.4 + (pulse * 0.6));
ctx.save();
ctx.globalCompositeOperation = "lighter";
const halo = ctx.createRadialGradient(0, 0, cell_px * 0.3, 0, 0, halo_radius);
halo.addColorStop(0.0, `rgba(239, 68, 68, ${0.45 + pulse * 0.35})`);
halo.addColorStop(0.5, `rgba(220, 38, 38, ${0.2 + pulse * 0.15})`);
halo.addColorStop(1.0, "rgba(239, 68, 68, 0.0)");
ctx.fillStyle = halo;
ctx.beginPath();
ctx.arc(0, 0, halo_radius, 0, Math.PI * 2);
ctx.fill();
ctx.save();
ctx.rotate(beacon_angle);
const beam_length = cell_px * 3.6;
const beam_grad = ctx.createRadialGradient(0, 0, 0, 0, 0, beam_length);
beam_grad.addColorStop(0.0, "rgba(254, 202, 202, 0.7)");
beam_grad.addColorStop(0.4, "rgba(239, 68, 68, 0.45)");
beam_grad.addColorStop(1.0, "rgba(239, 68, 68, 0.0)");
ctx.fillStyle = beam_grad;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, beam_length, -0.4, 0.4);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, beam_length, Math.PI - 0.4, Math.PI + 0.4);
ctx.closePath();
ctx.fill();
ctx.restore();
ctx.restore();
}
ctx.restore();
};
const constructor = () => {
const src = xgravity.settings.get("drone_image", null, "images/game/Louse.webp");
image = new Image();
image.onload = () => {
is_loaded = true;
};
image.src = src;
// Iniciar la animación de lanzamiento al crear la nave
self.start_deployment();
};
constructor();
};
DroneModel.drone_velocity = (px, py, dx, dy, vx, vy, f, delta = 0) => [
vx += (px - dx) * f * (delta > 0 ? delta : 1),
vy += (py - dy) * f * (delta > 0 ? delta : 1)
];
return DroneModel;
})();

View File

@ -0,0 +1,272 @@
"use strict";
const ExplosionsModel = (function(){
const ExplosionsModel = function(xgravity){
const self = this;
let particles = [];
let shockwaves = [];
let sparks = [];
this.add_sparks = (cell_x, cell_y, normal_x, normal_y, intensity = 1.0) => {
// A mayor intensidad de impacto, más chispas y más velocidad
const base_count = Math.floor(10 + (intensity * 25));
const count = Math.min(60, base_count);
const base_angle = Math.atan2(normal_y, normal_x);
for(let i = 0; i < count; i++){
// Cono de dispersión reflejado con +- 60 grados
const spread = (Math.random() - 0.5) * (Math.PI * 0.7);
const spark_angle = base_angle + spread;
const speed = (Math.random() * 12 + 6) * Math.min(2.5, intensity);
const life = (Math.random() * 0.18) + 0.08;
sparks.push({
x : cell_x,
y : cell_y,
vx : Math.cos(spark_angle) * speed,
vy : Math.sin(spark_angle) * speed,
len : (Math.random() * 0.3) + 0.15,
life : life,
max_life : life,
color : Math.random() > 0.3 ? "#fef08a" : "#f97316" // Chispas blanco-amarillo y naranja
});
}
};
const get_cell_size = (width, height) => {
const cells = xgravity.settings.get("cells", null, 40);
return (width < height ? width : height) / cells;
};
this.add = (cell_x, cell_y, type = "shield") => {
// 1. Delegar el audio al servicio central
if(xgravity.audio){
xgravity.audio.play(type === "shield" ? "shield_explosion" : "hit_explosion");
}
const is_shield = type === "shield";
// 2. Extraer parámetros visuales de Settings
const particles_range = xgravity.settings.get("explosion_particles_range", null, [45, 75]);
const spread_reach = xgravity.settings.get("explosion_particles_spread", null, 11.0);
const base_shockwave_radius = xgravity.settings.get("explosion_shockwave_radius", null, 4.5);
const min_p = Math.min(particles_range[0], particles_range[1]);
const max_p = Math.max(particles_range[0], particles_range[1]);
const count = Math.floor(min_p + Math.random() * (max_p - min_p + 1));
const max_radius = is_shield ? base_shockwave_radius : (base_shockwave_radius * 1.15);
// 3. Onda expansiva elíptica con inclinación aleatoria
shockwaves.push({
x : cell_x,
y : cell_y,
radius : 0.4,
max_radius : max_radius,
speed : is_shield ? 9.5 : 8.0,
angle : Math.random() * Math.PI * 2,
aspect_ratio : 0.42 + (Math.random() * 0.18),
alpha : 1.0,
color_rgb : is_shield ? "56, 189, 248" : "239, 68, 68"
});
// 4. Micropartículas esféricas difuminadas
for(let i = 0; i < count; i++){
const angle = Math.random() * Math.PI * 2;
const speed = (Math.random() * spread_reach) + 1.2;
const life = (Math.random() * 0.38) + 0.28;
let core_rgb = "255, 255, 255";
let rim_rgb = "56, 189, 248";
if(is_shield){
rim_rgb = Math.random() > 0.4 ? "56, 189, 248" : (Math.random() > 0.5 ? "186, 230, 253" : "2, 132, 199");
} else {
rim_rgb = Math.random() > 0.4 ? "239, 68, 68" : (Math.random() > 0.5 ? "249, 115, 22" : "251, 191, 36");
}
particles.push({
x : cell_x,
y : cell_y,
vx : Math.cos(angle) * speed,
vy : Math.sin(angle) * speed,
size : (Math.random() * 0.16) + 0.08,
life : life,
max_life : life,
core_rgb : core_rgb,
rim_rgb : rim_rgb
});
}
};
this.add_mothership_impact = (cx, cy) => {
// Flash de impacto en la cabina
particles.push({
type : "screen_flash",
alpha : 0.85,
color : "#ffffff",
fade_speed : 2.5
});
// 60-80 Fragmentos de roca y metralla de diversos tamaños
const count = 75;
for(let i = 0; i < count; i++){
const angle = Math.random() * Math.PI * 2;
const speed = 120 + (Math.random() * 650); // Píxeles/segundo
const size = 3 + (Math.random() * 18); // Desde gravilla hasta peñascos
const is_rock = Math.random() > 0.3;
particles.push({
type : is_rock ? "rock_shard" : "plasma_spark",
x : cx + ((Math.random() - 0.5) * 40),
y : cy + ((Math.random() - 0.5) * 40),
vx : Math.cos(angle) * speed,
vy : Math.sin(angle) * speed,
angle : Math.random() * Math.PI * 2,
angular_vel : (Math.random() - 0.5) * 12.0,
size : size,
life : 1.0,
decay : 0.45 + (Math.random() * 0.75),
color : is_rock ? (Math.random() > 0.5 ? "#78716c" : "#a8a29e") : (Math.random() > 0.5 ? "#f97316" : "#facc15"),
vertices : is_rock ? [
{ x: -size * 0.5, y: -size * 0.4 },
{ x: size * 0.6, y: -size * 0.3 },
{ x: size * 0.4, y: size * 0.5 },
{ x: -size * 0.3, y: size * 0.6 }
] : null
});
}
};
this.update = (delta_time) => {
for(let i = particles.length - 1; i >= 0; i--){
const p = particles[i];
p.life -= delta_time;
if(p.life <= 0){
particles.splice(i, 1);
continue;
}
p.x += p.vx * delta_time;
p.y += p.vy * delta_time;
p.vx *= 0.93;
p.vy *= 0.93;
}
for(let i = shockwaves.length - 1; i >= 0; i--){
const s = shockwaves[i];
s.radius += s.speed * delta_time;
s.alpha = Math.max(0, 1.0 - (s.radius / s.max_radius));
if(s.radius >= s.max_radius){
shockwaves.splice(i, 1);
}
}
for(let i = sparks.length - 1; i >= 0; i--){
const s = sparks[i];
s.life -= delta_time;
if(s.life <= 0){
sparks.splice(i, 1);
continue;
}
s.x += s.vx * delta_time;
s.y += s.vy * delta_time;
s.vx *= 0.90;
s.vy *= 0.90;
}
};
this.render = (ctx, width, height) => {
const cell_px = get_cell_size(width, height);
const cx = width / 2;
const cy = height / 2;
ctx.save();
ctx.globalCompositeOperation = "lighter";
// Ondas de choque elípticas
for(let i = 0; i < shockwaves.length; i++){
const s = shockwaves[i];
const px = cx + (s.x * cell_px);
const py = cy + (s.y * cell_px);
const r_px = s.radius * cell_px;
ctx.save();
ctx.translate(px, py);
ctx.rotate(s.angle);
ctx.scale(1.0, s.aspect_ratio);
ctx.lineWidth = cell_px * 0.14 * s.alpha;
ctx.strokeStyle = `rgba(${s.color_rgb}, ${s.alpha * 0.85})`;
ctx.beginPath();
ctx.arc(0, 0, r_px, 0, Math.PI * 2);
ctx.stroke();
ctx.lineWidth = cell_px * 0.05 * s.alpha;
ctx.strokeStyle = `rgba(255, 255, 255, ${s.alpha * 0.6})`;
ctx.beginPath();
ctx.arc(0, 0, r_px * 0.82, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
// Micropartículas
for(let i = 0; i < particles.length; i++){
const p = particles[i];
const px = cx + (p.x * cell_px);
const py = cy + (p.y * cell_px);
const r_px = p.size * cell_px;
const progress = Math.max(0, p.life / p.max_life);
const grad = ctx.createRadialGradient(px, py, 0, px, py, r_px);
grad.addColorStop(0.0, `rgba(${p.core_rgb}, ${progress})`);
grad.addColorStop(0.45, `rgba(${p.rim_rgb}, ${progress * 0.75})`);
grad.addColorStop(1.0, `rgba(${p.rim_rgb}, 0.0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(px, py, r_px, 0, Math.PI * 2);
ctx.fill();
}
// Renderizado de Chispas Cinéticas (sin delta_time)
for(let i = 0; i < sparks.length; i++){
const s = sparks[i];
const px = cx + (s.x * cell_px);
const py = cy + (s.y * cell_px);
const progress = Math.max(0, s.life / s.max_life);
// Calculamos la longitud y dirección de la estela en base a su velocidad
const spd = Math.sqrt(s.vx * s.vx + s.vy * s.vy);
const dir_x = spd > 0.001 ? s.vx / spd : 0;
const dir_y = spd > 0.001 ? s.vy / spd : 0;
// Longitud visible de la estela en píxeles (usa s.len escalado por la velocidad y vida)
const tail_length_px = (s.len || 0.25) * cell_px * Math.min(2.5, spd * 0.15);
ctx.lineWidth = cell_px * 0.06;
ctx.strokeStyle = s.color;
ctx.globalAlpha = progress;
ctx.beginPath();
ctx.moveTo(px, py);
// La estela se proyecta hacia atrás respecto a la trayectoria
ctx.lineTo(px - (dir_x * tail_length_px), py - (dir_y * tail_length_px));
ctx.stroke();
}
ctx.restore();
};
this.reset = () => {
particles = [];
shockwaves = [];
};
};
return ExplosionsModel;
})();

View File

@ -0,0 +1,105 @@
"use strict";
const RusherEnemyModel = (function(){
const RusherEnemyModel = function(xgravity, config = {}){
const self = this;
const entity = {
x : 0,
y : 0,
vx : 0,
vy : 0,
angle : 0,
scale : 1.0,
alpha : 1.0,
ready : false,
alive : true,
age : 0,
config : config
};
const base_size = config.size || 1.5;
const points = config.points || 200;
const mass = config.mass || 4.0;
let health = config.health || 20;
const max_health = health;
const image_src = config.image || null;
const spawn_strategy = SpawnStrategies.get(config.spawn_mode || "perimeter");
const behavior_strategy = BehaviorStrategies.get(config.behavior || "cross_screen");
this.is_ready = () => entity.ready;
this.is_alive = () => entity.alive;
this.get_points = () => points;
this.get_age = () => entity.age;
this.get_mass = () => mass;
this.get_health = () => health;
this.get_max_health = () => max_health;
this.get_velocity = () => ({ vx: entity.vx, vy: entity.vy });
this.get_state = () => ({
x : entity.x,
y : entity.y,
angle : entity.angle,
scale : entity.scale,
alpha : entity.alpha,
size : base_size,
image_src : image_src,
ready : entity.ready,
alive : entity.alive,
health : health,
max_health : max_health,
radius : (base_size * entity.scale) * 0.45,
fallback_color : entity.ready ? "#e11d48" : "#fb7185"
});
this.get_bounds = () => ({
x : entity.x,
y : entity.y,
radius : (base_size * entity.scale) * 0.45
});
this.apply_impulse = (ivx, ivy) => {
entity.vx += ivx;
entity.vy += ivy;
};
this.set_position = (nx, ny) => {
entity.x = nx;
entity.y = ny;
};
this.take_damage = (amount) => {
health = Math.max(0, health - amount);
if(health <= 0){
entity.alive = false;
}
return entity.alive;
};
this.on_hit = () => {
entity.alive = false;
};
this.update = (delta_time, drone_info) => {
if(!entity.alive) return;
entity.age += delta_time;
if(!entity.ready){
spawn_strategy.update(entity, delta_time);
} else {
behavior_strategy(entity, delta_time, drone_info);
}
};
const constructor = () => {
spawn_strategy.init(entity, config, xgravity);
};
constructor();
};
return RusherEnemyModel;
})();

View File

@ -0,0 +1,105 @@
"use strict";
const StaticMineEnemyModel = (function(){
const StaticMineEnemyModel = function(xgravity, config = {}){
const self = this;
const entity = {
x : 0,
y : 0,
vx : 0,
vy : 0,
angle : Math.random() * Math.PI * 2,
scale : 0.0,
alpha : 0.3,
ready : false,
alive : true,
age : 0,
config : config
};
const base_size = config.size || 2.0;
const points = config.points || 50;
const mass = config.mass || 5.0;
let health = config.health || 40;
const max_health = health;
const image_src = config.image || null;
const spawn_strategy = SpawnStrategies.get(config.spawn_mode || "deep_space");
const behavior_strategy = BehaviorStrategies.get(config.behavior || "static");
this.is_ready = () => entity.ready;
this.is_alive = () => entity.alive;
this.get_points = () => points;
this.get_age = () => entity.age;
this.get_mass = () => mass;
this.get_health = () => health;
this.get_max_health = () => max_health;
this.get_velocity = () => ({ vx: entity.vx, vy: entity.vy });
this.get_state = () => ({
x : entity.x,
y : entity.y,
angle : entity.angle,
scale : entity.scale,
alpha : entity.alpha,
size : base_size,
image_src : image_src,
ready : entity.ready,
alive : entity.alive,
health : health,
max_health : max_health,
radius : (base_size * entity.scale) * 0.45,
fallback_color : entity.ready ? "#ef4444" : "#f59e0b"
});
this.get_bounds = () => ({
x : entity.x,
y : entity.y,
radius : (base_size * entity.scale) * 0.45
});
this.apply_impulse = (ivx, ivy) => {
entity.vx += ivx;
entity.vy += ivy;
};
this.set_position = (nx, ny) => {
entity.x = nx;
entity.y = ny;
};
this.take_damage = (amount) => {
health = Math.max(0, health - amount);
if(health <= 0){
entity.alive = false;
}
return entity.alive;
};
this.on_hit = () => {
entity.alive = false;
};
this.update = (delta_time, drone_info) => {
if(!entity.alive) return;
entity.age += delta_time;
if(!entity.ready){
spawn_strategy.update(entity, delta_time);
} else {
behavior_strategy(entity, delta_time, drone_info);
}
};
const constructor = () => {
spawn_strategy.init(entity, config, xgravity);
};
constructor();
};
return StaticMineEnemyModel;
})();

View File

@ -0,0 +1,442 @@
"use strict";
const TelemetryHudModel = (function(){
const TelemetryHudModel = function(xgravity){
const self = this;
let blink_timer = 0;
let current_score = 0;
// Vidas dinámicas según dificultad o configuración
const get_initial_lives = () => {
const preset = (xgravity.settings && typeof xgravity.settings.get_difficulty_preset === "function")
? xgravity.settings.get_difficulty_preset()
: null;
return (preset && preset.drone_lives !== undefined)
? preset.drone_lives
: xgravity.settings.get("drone_lives", null, 3);
};
let current_lives = get_initial_lives();
let max_lives = current_lives;
const get_cell_size = (width, height) => {
const cells = xgravity.settings.get("cells", null, 40);
return (width < height ? width : height) / cells;
};
this.set_score = (val) => {
current_score = Math.max(0, Math.floor(val));
};
this.add_score = (val) => {
current_score += Math.max(0, Math.floor(val));
};
this.get_score = () => current_score;
this.set_lives = (lives, max = null) => {
current_lives = Math.max(0, Math.floor(lives));
if(max !== null){
max_lives = Math.max(1, Math.floor(max));
}
};
this.get_lives = () => current_lives;
// Dibuja texto con contorno perimetral oscuro de alto contraste
const draw_hud_text = (ctx, text, x, y, fill_color, cell_px) => {
ctx.save();
ctx.lineWidth = cell_px * 0.28;
ctx.strokeStyle = "rgba(2, 6, 23, 0.95)";
ctx.lineJoin = "round";
ctx.strokeText(text, x, y);
ctx.fillStyle = fill_color;
ctx.fillText(text, x, y);
ctx.restore();
};
// Dibuja silueta táctica de caza/dron en la barra de reservas
const draw_drone_life_pip = (ctx, x, y, size, is_active, color, cell_px) => {
ctx.save();
ctx.translate(x, y);
ctx.beginPath();
ctx.moveTo(0, -size * 0.45);
ctx.lineTo(size * 0.42, size * 0.45);
ctx.lineTo(0, size * 0.22);
ctx.lineTo(-size * 0.42, size * 0.45);
ctx.closePath();
if(is_active){
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = cell_px * 0.35;
ctx.fill();
ctx.lineWidth = cell_px * 0.08;
ctx.strokeStyle = "rgba(255, 255, 255, 0.85)";
ctx.stroke();
} else {
ctx.fillStyle = "rgba(2, 6, 23, 0.75)";
ctx.fill();
ctx.lineWidth = cell_px * 0.07;
ctx.strokeStyle = "rgba(56, 189, 248, 0.25)";
ctx.stroke();
}
ctx.restore();
};
// Dibuja una barra táctica con centro en 0 (bipolar: -1 a +1)
const draw_bipolar_bar = (ctx, x, y, width, height, value, fill_color, cell_px) => {
ctx.save();
ctx.fillStyle = "rgba(2, 6, 23, 0.88)";
ctx.fillRect(x - (cell_px * 0.1), y - (cell_px * 0.1), width + (cell_px * 0.2), height + (cell_px * 0.2));
ctx.strokeStyle = "rgba(56, 189, 248, 0.65)";
ctx.lineWidth = cell_px * 0.09;
ctx.strokeRect(x, y, width, height);
const mid_x = x + (width / 2);
ctx.beginPath();
ctx.moveTo(mid_x, y);
ctx.lineTo(mid_x, y + height);
ctx.strokeStyle = "rgba(255, 255, 255, 0.85)";
ctx.lineWidth = cell_px * 0.08;
ctx.stroke();
const clamped = Math.max(-1.0, Math.min(1.0, value));
const bar_length = (width / 2) * Math.abs(clamped);
ctx.fillStyle = fill_color;
if(clamped > 0){
ctx.fillRect(mid_x, y, bar_length, height);
} else if(clamped < 0){
ctx.fillRect(mid_x - bar_length, y, bar_length, height);
}
ctx.restore();
};
// Dibuja una barra segmentada genérica (Escudo y Nodriza)
const draw_segmented_bar = (ctx, x, y, width, height, pct, fill_color, cell_px, segments = 10) => {
ctx.save();
const gap = cell_px * 0.12;
const seg_width = (width - (gap * (segments - 1))) / segments;
ctx.fillStyle = "rgba(2, 6, 23, 0.88)";
ctx.fillRect(x - (cell_px * 0.1), y - (cell_px * 0.1), width + (cell_px * 0.2), height + (cell_px * 0.2));
for(let s = 0; s < segments; s++){
const seg_x = x + (s * (seg_width + gap));
const seg_threshold = (s + 1) / segments;
ctx.strokeStyle = "rgba(56, 189, 248, 0.4)";
ctx.lineWidth = cell_px * 0.07;
ctx.strokeRect(seg_x, y, seg_width, height);
if(pct >= seg_threshold - 0.05){
ctx.fillStyle = fill_color;
ctx.fillRect(seg_x, y, seg_width, height);
}
}
ctx.restore();
};
// Dibuja las marcas perimétricas que siguen a la nave en X e Y
const draw_axis_tracking_ticks = (ctx, cx, cy, drone_px_x, drone_px_y, radius_px, color, cell_px) => {
ctx.save();
const tick_len = cell_px * 1.0;
const gap = cell_px * 0.4;
ctx.strokeStyle = color;
ctx.lineWidth = cell_px * 0.12;
ctx.lineCap = "round";
const x_pos = drone_px_x;
const top_base_y = cy - radius_px - gap;
ctx.beginPath();
ctx.moveTo(x_pos, top_base_y);
ctx.lineTo(x_pos, top_base_y - tick_len);
ctx.stroke();
const btm_base_y = cy + radius_px + gap;
ctx.beginPath();
ctx.moveTo(x_pos, btm_base_y);
ctx.lineTo(x_pos, btm_base_y + tick_len);
ctx.stroke();
const y_pos = drone_px_y;
const left_base_x = cx - radius_px - gap;
ctx.beginPath();
ctx.moveTo(left_base_x, y_pos);
ctx.lineTo(left_base_x - tick_len, y_pos);
ctx.stroke();
const right_base_x = cx + radius_px + gap;
ctx.beginPath();
ctx.moveTo(right_base_x, y_pos);
ctx.lineTo(right_base_x + tick_len, y_pos);
ctx.stroke();
ctx.restore();
};
this.render = (ctx, width, height, delta_time, drone_model, cockpit_model, score_val = null, lives_val = null, max_lives_val = null) => {
if(!drone_model) return;
if(score_val !== null){
current_score = score_val;
}
if(lives_val !== null){
current_lives = Math.max(0, Math.floor(lives_val));
}
if(max_lives_val !== null){
max_lives = Math.max(1, Math.floor(max_lives_val));
}
const cell_px = get_cell_size(width, height);
const cx = width / 2;
const cy = height / 2;
blink_timer += delta_time;
const safe_radius_cells = xgravity.settings.get("safe_zone_radius", null, 18.5);
const warn_radius_cells = xgravity.settings.get("safe_zone_warning", null, 13.5);
const safe_radius_px = safe_radius_cells * cell_px;
const drone_info = drone_model.get_position();
// 1. Coordenadas y distancias en celdas
const dcx = drone_info.cell_x || 0;
const dcy = drone_info.cell_y || 0;
const dist_cells = Math.sqrt(dcx * dcx + dcy * dcy);
const ptr_cx = drone_info.target_cell_x || 0;
const ptr_cy = drone_info.target_cell_y || 0;
const ptr_dist_cells = Math.sqrt(ptr_cx * ptr_cx + ptr_cy * ptr_cy);
const v_cell_x = drone_info.vel_cell_x || 0;
const v_cell_y = drone_info.vel_cell_y || 0;
const drone_px_x = cx + (dcx * cell_px);
const drone_px_y = cy + (dcy * cell_px);
// 2. Alertas
let drone_danger_alpha = 0;
const is_drone_lost = dist_cells > safe_radius_cells;
if(dist_cells > warn_radius_cells){
drone_danger_alpha = Math.min((dist_cells - warn_radius_cells) / (safe_radius_cells - warn_radius_cells), 1.0);
}
let ptr_danger_alpha = 0;
const is_ptr_lost = ptr_dist_cells > safe_radius_cells;
if(ptr_dist_cells > warn_radius_cells){
ptr_danger_alpha = Math.min((ptr_dist_cells - warn_radius_cells) / (safe_radius_cells - warn_radius_cells), 1.0);
}
if(cockpit_model && typeof cockpit_model.set_alert === "function"){
cockpit_model.set_alert(drone_danger_alpha > 0.4 || is_drone_lost || ptr_danger_alpha > 0.4 || is_ptr_lost || current_lives <= 1);
}
// Colores dinámicos
const drone_color = is_drone_lost
? ((Math.sin(blink_timer * 14) > 0) ? "#ef4444" : "#7f1d1d")
: (drone_danger_alpha > 0 ? "#f59e0b" : "#38bdf8");
const ptr_color = is_ptr_lost
? ((Math.sin(blink_timer * 14) > 0) ? "#ef4444" : "#7f1d1d")
: (ptr_danger_alpha > 0 ? "#f59e0b" : "#c084fc");
ctx.save();
// 3. Perímetro táctico centrado
ctx.save();
ctx.lineWidth = cell_px * 0.16;
ctx.strokeStyle = "rgba(0, 0, 0, 0.7)";
ctx.beginPath();
ctx.arc(cx, cy, safe_radius_px, 0, Math.PI * 2);
ctx.stroke();
ctx.lineWidth = cell_px * 0.1;
ctx.setLineDash([cell_px * 0.5, cell_px * 0.35]);
ctx.strokeStyle = (drone_danger_alpha > 0 || ptr_danger_alpha > 0)
? `rgba(239, 68, 68, ${0.35 + Math.max(drone_danger_alpha, ptr_danger_alpha) * 0.65})`
: "rgba(56, 189, 248, 0.45)";
ctx.stroke();
ctx.restore();
// 4. LÍNEAS DE POSICIÓN PERIMÉTRICAS
draw_axis_tracking_ticks(ctx, cx, cy, drone_px_x, drone_px_y, safe_radius_px, drone_color, cell_px);
// 5. Degradado de escape
if(drone_danger_alpha > 0){
const drone_angle = Math.atan2(dcy, dcx);
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(drone_angle);
const grad_x = safe_radius_px;
const grad_radius = cell_px * 18.5;
const grad = ctx.createRadialGradient(grad_x, 0, 0, grad_x, 0, grad_radius);
const peak_color = is_drone_lost ? "rgba(220, 38, 38, " : "rgba(239, 68, 68, ";
grad.addColorStop(0, `${peak_color}${drone_danger_alpha * 0.55})`);
grad.addColorStop(0.5, `${peak_color}${drone_danger_alpha * 0.22})`);
grad.addColorStop(1, "rgba(239, 68, 68, 0)");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(grad_x, 0, grad_radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// 6. Bloque de telemetría en esquina inferior izquierda (8 líneas)
const margin = cell_px * 1.0;
const hud_x = margin;
const line_spacing = cell_px * 1.35;
const hud_y = height - margin - (line_spacing * 7);
ctx.font = `bold ${cell_px * 0.95}px monospace`;
// Línea 0: Puntuación (SCR)
const score_str = current_score.toString().padStart(8, "0");
draw_hud_text(ctx, `SCR : [${score_str}]`, hud_x, hud_y, "#38bdf8", cell_px);
// Línea 1: Vidas de Drones (LVS)
const bar_w = cell_px * 8.5;
const bar_h = cell_px * 0.5;
const bar_start_x = hud_x + (cell_px * 16.5);
const lvs_y = hud_y + line_spacing;
let lvs_color = "#38bdf8";
if(current_lives <= 1){
lvs_color = (Math.sin(blink_timer * 12) > 0) ? "#ef4444" : "#7f1d1d";
}
draw_hud_text(ctx, `LVS : [ ${current_lives} ]`, hud_x, lvs_y, lvs_color, cell_px);
// Iconos de cazas/drones en la barra táctica de reservas
const slot_w = Math.min(cell_px * 1.6, bar_w / Math.max(1, max_lives));
const icon_size = cell_px * 0.9;
for(let i = 0; i < max_lives; i++){
const pip_x = bar_start_x + (i * slot_w) + (slot_w * 0.5);
const pip_y = lvs_y - (cell_px * 0.32);
const is_active = i < current_lives;
draw_drone_life_pip(ctx, pip_x, pip_y, icon_size, is_active, lvs_color, cell_px);
}
// Línea 2: Dron (DRN)
const pos_cells_x = dcx.toFixed(2);
const pos_cells_y = dcy.toFixed(2);
draw_hud_text(ctx, `DRN : X[${pos_cells_x >= 0 ? "+" : ""}${pos_cells_x}] Y[${pos_cells_y >= 0 ? "+" : ""}${pos_cells_y}]`, hud_x, hud_y + (line_spacing * 2), drone_color, cell_px);
draw_bipolar_bar(ctx, bar_start_x, (hud_y + (line_spacing * 2)) - (cell_px * 0.75), bar_w, bar_h, dcx / safe_radius_cells, drone_color, cell_px);
// Línea 3: Puntero (PTR)
const ptr_x = ptr_cx.toFixed(2);
const ptr_y = ptr_cy.toFixed(2);
draw_hud_text(ctx, `PTR : X[${ptr_x >= 0 ? "+" : ""}${ptr_x}] Y[${ptr_y >= 0 ? "+" : ""}${ptr_y}]`, hud_x, hud_y + (line_spacing * 3), ptr_color, cell_px);
draw_bipolar_bar(ctx, bar_start_x, (hud_y + (line_spacing * 3)) - (cell_px * 0.75), bar_w, bar_h, ptr_cx / safe_radius_cells, ptr_color, cell_px);
// Línea 4: Velocidad (SPD)
const spd_x = v_cell_x.toFixed(2);
const spd_y = v_cell_y.toFixed(2);
draw_hud_text(ctx, `SPD : X[${spd_x >= 0 ? "+" : ""}${spd_x}] Y[${spd_y >= 0 ? "+" : ""}${spd_y}]`, hud_x, hud_y + (line_spacing * 4), "#10b981", cell_px);
const current_speed_cells = Math.sqrt(v_cell_x * v_cell_x + v_cell_y * v_cell_y);
const spd_pct = Math.min(current_speed_cells / 15.0, 1.0);
ctx.fillStyle = "rgba(2, 6, 23, 0.88)";
ctx.fillRect(bar_start_x - (cell_px * 0.1), (hud_y + (line_spacing * 4)) - (cell_px * 0.75) - (cell_px * 0.1), bar_w + (cell_px * 0.2), bar_h + (cell_px * 0.2));
ctx.strokeStyle = "rgba(56, 189, 248, 0.65)";
ctx.lineWidth = cell_px * 0.09;
ctx.strokeRect(bar_start_x, (hud_y + (line_spacing * 4)) - (cell_px * 0.75), bar_w, bar_h);
ctx.fillStyle = "#10b981";
ctx.fillRect(bar_start_x, (hud_y + (line_spacing * 4)) - (cell_px * 0.75), bar_w * spd_pct, bar_h);
// Línea 5: Bloque de Escudo del Dron (SHD)
const shield_info = drone_info.shield !== undefined
? { current : drone_info.shield, max : drone_info.max_shield || 100 }
: { current : 100, max : 100 };
const shield_pct = Math.max(0, Math.min(1.0, shield_info.current / shield_info.max));
const shield_y = hud_y + (line_spacing * 5);
let shield_color = "#38bdf8";
let is_critical_shield = false;
if(shield_pct <= 0.25){
shield_color = (Math.sin(blink_timer * 12) > 0) ? "#ef4444" : "#7f1d1d";
is_critical_shield = true;
} else if(shield_pct <= 0.55){
shield_color = "#f59e0b";
}
draw_hud_text(ctx, `SHD : [${(shield_pct * 100).toFixed(0).padStart(3, " ")}%]`, hud_x, shield_y, shield_color, cell_px);
draw_segmented_bar(ctx, bar_start_x, shield_y - (cell_px * 0.75), bar_w, bar_h, shield_pct, shield_color, cell_px, 10);
// Línea 6: Integridad de la Nave Nodriza / Cabina (MTH)
let mothership_pct = 1.0;
if(cockpit_model && typeof cockpit_model.get_health_percentage === "function"){
mothership_pct = cockpit_model.get_health_percentage();
} else if(cockpit_model && typeof cockpit_model.get_hull === "function"){
mothership_pct = cockpit_model.get_hull() / 100.0;
} else if(typeof drone_info.mothership_pct === "number"){
mothership_pct = drone_info.mothership_pct;
}
mothership_pct = Math.max(0, Math.min(1.0, mothership_pct));
const mth_y = hud_y + (line_spacing * 6);
let mth_color = "#10b981";
let is_critical_mth = false;
if(mothership_pct <= 0.25){
mth_color = (Math.sin(blink_timer * 12) > 0) ? "#ef4444" : "#7f1d1d";
is_critical_mth = true;
} else if(mothership_pct <= 0.55){
mth_color = "#f59e0b";
}
draw_hud_text(ctx, `MTH : [${(mothership_pct * 100).toFixed(0).padStart(3, " ")}%]`, hud_x, mth_y, mth_color, cell_px);
draw_segmented_bar(ctx, bar_start_x, mth_y - (cell_px * 0.75), bar_w, bar_h, mothership_pct, mth_color, cell_px, 10);
// Línea 7: Estado del Enlace (LINK)
const link_y = hud_y + (line_spacing * 7);
let status_text = "LINK: OPTIMAL";
let status_color = "#38bdf8";
if(current_lives <= 0){
status_text = "DRONES DEPLETED - MISSION FAILED";
status_color = "#ef4444";
} else if(current_lives === 1){
status_text = (Math.sin(blink_timer * 12) > 0) ? "WARN: LAST DRONE STANDING!" : "WARN: CRITICAL RESERVES";
status_color = "#ef4444";
} else if(is_drone_lost || is_ptr_lost){
status_text = (Math.sin(blink_timer * 15) > 0) ? "LINK: SIGNAL LOST!" : "";
status_color = "#ef4444";
} else if(drone_danger_alpha > 0 || ptr_danger_alpha > 0){
status_text = (Math.sin(blink_timer * 8) > 0) ? "LINK: BOUNDARY WARNING" : "LINK: UNSTABLE";
status_color = "#f59e0b";
} else if(is_critical_mth){
status_text = "WARN: MOTHERSHIP CRITICAL";
status_color = "#ef4444";
} else if(is_critical_shield){
status_text = "WARN: SHIELD CRITICAL";
status_color = "#ef4444";
}
draw_hud_text(ctx, status_text, hud_x, link_y, status_color, cell_px);
ctx.restore();
};
};
return TelemetryHudModel;
})();

View File

@ -0,0 +1,82 @@
"use strict";
const AsteroidThreatAlarmSound = (function(){
return function(audio_ctx, output_node, volume_multiplier = 1.0, threat_level = 0.5){
const now = audio_ctx.currentTime;
const threat = Math.max(0.0, Math.min(1.0, threat_level));
// Bus maestro de cabina
const master_bus = audio_ctx.createGain();
master_bus.gain.setValueAtTime(volume_multiplier * 0.5, now);
// Filtro de megafonía/altavoz de instrumentación (elimina graves y agudos molestos)
const horn_filter = audio_ctx.createBiquadFilter();
horn_filter.type = "bandpass";
horn_filter.frequency.setValueAtTime(1150, now);
horn_filter.Q.setValueAtTime(1.8, now);
master_bus.connect(horn_filter);
horn_filter.connect(output_node);
// Función que sintetiza un 'BEEP' limpio de aviónica
const trigger_avionics_beep = (start_time, freq) => {
const beep_duration = 0.065; // 65ms: corto, clínico y penetrante
// Envolvente de amplitud trapezoidal (ataque 4ms, salida 8ms)
const env = audio_ctx.createGain();
env.gain.setValueAtTime(0.0001, start_time);
env.gain.linearRampToValueAtTime(0.85, start_time + 0.004);
env.gain.setValueAtTime(0.85, start_time + beep_duration - 0.008);
env.gain.exponentialRampToValueAtTime(0.0001, start_time + beep_duration);
// 1. Tono fundamental limpio
const osc_main = audio_ctx.createOscillator();
osc_main.type = "sine";
osc_main.frequency.setValueAtTime(freq, start_time);
// 2. Segundo armónico discreto (le da presencia de instrumento de cabina)
const osc_harm = audio_ctx.createOscillator();
const harm_gain = audio_ctx.createGain();
osc_harm.type = "sine";
osc_harm.frequency.setValueAtTime(freq * 2, start_time);
harm_gain.gain.setValueAtTime(0.18, start_time);
// 3. Toque de onda triangular sutil para cuerpo mecánico
const osc_tri = audio_ctx.createOscillator();
const tri_gain = audio_ctx.createGain();
osc_tri.type = "triangle";
osc_tri.frequency.setValueAtTime(freq, start_time);
tri_gain.gain.setValueAtTime(0.12, start_time);
osc_main.connect(env);
osc_harm.connect(harm_gain);
harm_gain.connect(env);
osc_tri.connect(tri_gain);
tri_gain.connect(env);
env.connect(master_bus);
osc_main.start(start_time);
osc_harm.start(start_time);
osc_tri.start(start_time);
osc_main.stop(start_time + beep_duration);
osc_harm.stop(start_time + beep_duration);
osc_tri.stop(start_time + beep_duration);
};
// Frecuencia estándar de advertencia: 1020 Hz (tono aeronáutico estándar)
// Si la piedra viene de camino a la cabina (> 0.6), sube ligeramente a 1280 Hz
const base_freq = threat > 0.6 ? 1280 : 1020;
// Primer beep
trigger_avionics_beep(now, base_freq);
// Si el peligro es inminente (la roca ya fue lanzada a cabina), emite un doble beep militar ("PIP-PIP")
if(threat > 0.6){
trigger_avionics_beep(now + 0.09, base_freq);
}
};
})();

View File

@ -0,0 +1,52 @@
"use strict";
const HitExplosionSound = (function(){
return function(audio_ctx, output_node, volume_multiplier = 1.0){
const now = audio_ctx.currentTime;
const buffer_size = audio_ctx.sampleRate * 0.48;
const buffer = audio_ctx.createBuffer(1, buffer_size, audio_ctx.sampleRate);
const data = buffer.getChannelData(0);
for(let i = 0; i < buffer_size; i++){
data[i] = (Math.random() * 2) - 1;
}
const noise = audio_ctx.createBufferSource();
noise.buffer = buffer;
const filter = audio_ctx.createBiquadFilter();
const gain = audio_ctx.createGain();
// Ruido sordo filtrado en graves
filter.type = "lowpass";
filter.frequency.setValueAtTime(420, now);
filter.frequency.exponentialRampToValueAtTime(32, now + 0.48);
gain.gain.setValueAtTime(0.9 * volume_multiplier, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.48);
noise.connect(filter);
filter.connect(gain);
gain.connect(output_node);
// Subgrave sintetizado para dar golpe de casco en el vacío
const osc = audio_ctx.createOscillator();
const osc_gain = audio_ctx.createGain();
osc.type = "triangle";
osc.frequency.setValueAtTime(130, now);
osc.frequency.exponentialRampToValueAtTime(25, now + 0.35);
osc_gain.gain.setValueAtTime(0.8 * volume_multiplier, now);
osc_gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
osc.connect(osc_gain);
osc_gain.connect(output_node);
osc.start(now);
osc.stop(now + 0.35);
noise.start(now);
noise.stop(now + 0.48);
};
})();

View File

@ -0,0 +1,136 @@
"use strict";
const MothershipImpactSound = (function(){
// Curva de saturación analógica para darle máxima pegada sin romper el audio
const make_distortion_curve = (amount = 20) => {
const samples = 44100;
const curve = new Float32Array(samples);
const deg = Math.PI / 180;
for(let i = 0; i < samples; ++i){
const x = (i * 2) / samples - 1;
curve[i] = ((3 + amount) * x * 20 * deg) / (Math.PI + amount * Math.abs(x));
}
return curve;
};
return function(audio_ctx, output_node, volume_multiplier = 1.0){
const now = audio_ctx.currentTime;
const duration = 2.4; // Retumbe largo y cavernoso
// Bus maestro del efecto con booster de presencia
const master_bus = audio_ctx.createGain();
master_bus.gain.setValueAtTime(volume_multiplier * 1.35, now);
// Saturador suave para sonido "aplastante"
const shaper = audio_ctx.createWaveShaper();
shaper.curve = make_distortion_curve(18);
shaper.oversample = "2x";
// ========================================================
// CAPA 1: SUB-GRAVE SÍSMICO (Terremoto acústico en cabina)
// ========================================================
const sub_osc = audio_ctx.createOscillator();
const sub_gain = audio_ctx.createGain();
sub_osc.type = "sine";
sub_osc.frequency.setValueAtTime(145, now);
// Caída drástica a frecuencias casi inaudibles pero que mueven aire
sub_osc.frequency.exponentialRampToValueAtTime(22, now + 0.45);
sub_osc.frequency.setValueAtTime(20, now + duration);
sub_gain.gain.setValueAtTime(0.001, now);
sub_gain.gain.linearRampToValueAtTime(1.0, now + 0.015);
sub_gain.gain.exponentialRampToValueAtTime(0.001, now + 1.8);
sub_osc.connect(sub_gain);
sub_gain.connect(shaper);
// ========================================================
// CAPA 2: CRUJIDO DE CASCO Y METRALLA (Ráfaga de ruido blanco)
// ========================================================
const buffer_size = Math.floor(audio_ctx.sampleRate * 1.5);
const noise_buffer = audio_ctx.createBuffer(1, buffer_size, audio_ctx.sampleRate);
const noise_data = noise_buffer.getChannelData(0);
for(let i = 0; i < buffer_size; i++){
noise_data[i] = (Math.random() * 2 - 1);
}
const noise_src = audio_ctx.createBufferSource();
noise_src.buffer = noise_buffer;
const noise_filter = audio_ctx.createBiquadFilter();
noise_filter.type = "lowpass";
noise_filter.frequency.setValueAtTime(4200, now);
noise_filter.frequency.exponentialRampToValueAtTime(110, now + 1.2);
noise_filter.Q.setValueAtTime(2.5, now);
const noise_env = audio_ctx.createGain();
noise_env.gain.setValueAtTime(0.001, now);
noise_env.gain.linearRampToValueAtTime(0.9, now + 0.02);
noise_env.gain.exponentialRampToValueAtTime(0.001, now + 1.4);
noise_src.connect(noise_filter);
noise_filter.connect(noise_env);
noise_env.connect(shaper);
// ========================================================
// CAPA 3: DEFORMACIÓN METÁLICA (Resonador desafinado)
// ========================================================
const metal_osc_a = audio_ctx.createOscillator();
const metal_osc_b = audio_ctx.createOscillator();
const metal_gain = audio_ctx.createGain();
metal_osc_a.type = "sawtooth";
metal_osc_b.type = "triangle";
metal_osc_a.frequency.setValueAtTime(78, now);
metal_osc_a.frequency.exponentialRampToValueAtTime(32, now + 0.9);
metal_osc_b.frequency.setValueAtTime(114, now);
metal_osc_b.frequency.exponentialRampToValueAtTime(38, now + 0.9);
metal_gain.gain.setValueAtTime(0.001, now);
metal_gain.gain.linearRampToValueAtTime(0.55, now + 0.04);
metal_gain.gain.exponentialRampToValueAtTime(0.001, now + 1.1);
metal_osc_a.connect(metal_gain);
metal_osc_b.connect(metal_gain);
metal_gain.connect(shaper);
// ========================================================
// CAPA 4: COLA DE REVERBERACIÓN DE LA NAVE (Eco retumbante)
// ========================================================
const rumble_delay = audio_ctx.createDelay();
rumble_delay.delayTime.setValueAtTime(0.075, now);
const rumble_fb = audio_ctx.createGain();
rumble_fb.gain.setValueAtTime(0.48, now);
const rumble_filter = audio_ctx.createBiquadFilter();
rumble_filter.type = "lowpass";
rumble_filter.frequency.setValueAtTime(320, now);
rumble_delay.connect(rumble_filter);
rumble_filter.connect(rumble_fb);
rumble_fb.connect(rumble_delay);
rumble_filter.connect(master_bus);
// Conexiones de salida
shaper.connect(master_bus);
shaper.connect(rumble_delay);
master_bus.connect(output_node);
// Disparo coordinado de los generadores
sub_osc.start(now);
noise_src.start(now);
metal_osc_a.start(now);
metal_osc_b.start(now);
sub_osc.stop(now + duration);
noise_src.stop(now + 1.5);
metal_osc_a.stop(now + 1.2);
metal_osc_b.stop(now + 1.2);
};
})();

View File

@ -0,0 +1,51 @@
"use strict";
const PerimeterWarningSound = (function(){
return function(audio_ctx, output_node, volume_multiplier = 1.0){
const now = audio_ctx.currentTime;
const duration = 0.14;
const preset_bus = audio_ctx.createGain();
preset_bus.gain.setValueAtTime(volume_multiplier * 0.5, now);
// Envolvente seca estilo sonar
const env = audio_ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.85, now + 0.01);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
// Tono puro resonante de telemetría militar (960Hz descendiendo a 820Hz)
const osc = audio_ctx.createOscillator();
osc.type = "sine";
osc.frequency.setValueAtTime(960, now);
osc.frequency.exponentialRampToValueAtTime(820, now + duration);
// Armónico metálico sutil
const harm = audio_ctx.createOscillator();
const harm_gain = audio_ctx.createGain();
harm.type = "triangle";
harm.frequency.setValueAtTime(1920, now);
harm_gain.gain.setValueAtTime(0.2, now);
// Filtro de mampara
const filter = audio_ctx.createBiquadFilter();
filter.type = "bandpass";
filter.frequency.setValueAtTime(900, now);
filter.Q.setValueAtTime(2.5, now);
osc.connect(env);
harm.connect(harm_gain);
harm_gain.connect(env);
env.connect(filter);
filter.connect(preset_bus);
preset_bus.connect(output_node);
osc.start(now);
harm.start(now);
osc.stop(now + duration);
harm.stop(now + duration);
};
})();

View File

@ -0,0 +1,87 @@
"use strict";
const ShieldExplosionSound = (function(){
return function(audio_ctx, output_node, volume_multiplier = 1.0){
const now = audio_ctx.currentTime;
const duration = 1.6; // Explosión profunda y larga
const preset_bus = audio_ctx.createGain();
preset_bus.gain.setValueAtTime(volume_multiplier, now);
// 1. RUIDO BROWNIANO / ROJO (El trueno profundo)
// Genera una acumulación continua de bajas frecuencias (cero agudos)
const buffer_size = Math.floor(audio_ctx.sampleRate * duration);
const buffer = audio_ctx.createBuffer(1, buffer_size, audio_ctx.sampleRate);
const data = buffer.getChannelData(0);
let last_val = 0.0;
for(let i = 0; i < buffer_size; i++){
const white = (Math.random() * 2) - 1;
// Integración para convertir ruido blanco en browniano (grave puro)
last_val = (last_val + (0.02 * white)) / 1.02;
data[i] = last_val * 3.5;
}
const rumble_source = audio_ctx.createBufferSource();
rumble_source.buffer = buffer;
// Filtro paso-bajo muy estricto: solo deja pasar el retumbe sordo
const rumble_filter = audio_ctx.createBiquadFilter();
rumble_filter.type = "lowpass";
rumble_filter.frequency.setValueAtTime(140, now);
rumble_filter.frequency.exponentialRampToValueAtTime(30, now + duration);
const rumble_gain = audio_ctx.createGain();
rumble_gain.gain.setValueAtTime(1.2, now);
// El estruendo decae lentamente como una onda expansiva real
rumble_gain.gain.exponentialRampToValueAtTime(0.001, now + duration);
rumble_source.connect(rumble_filter);
rumble_filter.connect(rumble_gain);
rumble_gain.connect(preset_bus);
rumble_source.start(now);
rumble_source.stop(now + duration);
// 2. ONDA DE CHOQUE SUBGRAVE (El golpe sísmico en el pecho)
// Desciende a frecuencias casi inaudibles pero que mueven la membrana de los cascos
const sub_osc = audio_ctx.createOscillator();
const sub_gain = audio_ctx.createGain();
sub_osc.type = "sine";
sub_osc.frequency.setValueAtTime(68, now);
sub_osc.frequency.exponentialRampToValueAtTime(18, now + 0.85);
sub_gain.gain.setValueAtTime(1.4, now);
sub_gain.gain.exponentialRampToValueAtTime(0.001, now + 0.85);
sub_osc.connect(sub_gain);
sub_gain.connect(preset_bus);
sub_osc.start(now);
sub_osc.stop(now + 0.85);
// 3. SEGUNDO OSCILADOR DE GRUÑIDO (Rumble modulado)
// Aporta fricción y textura de metal/masa estallando en el vacío
const rumble_osc = audio_ctx.createOscillator();
const rumble_osc_gain = audio_ctx.createGain();
rumble_osc.type = "triangle";
rumble_osc.frequency.setValueAtTime(45, now);
rumble_osc.frequency.exponentialRampToValueAtTime(12, now + 1.2);
rumble_osc_gain.gain.setValueAtTime(0.7, now);
rumble_osc_gain.gain.exponentialRampToValueAtTime(0.001, now + 1.2);
rumble_osc.connect(rumble_osc_gain);
rumble_osc_gain.connect(preset_bus);
rumble_osc.start(now);
rumble_osc.stop(now + 1.2);
// Salida directa hacia el bus maestro y limitador
preset_bus.connect(output_node);
};
})();

View File

@ -0,0 +1,94 @@
"use strict";
const ShieldLowAlarmSound = (function(){
return function(audio_ctx, output_node, volume_multiplier = 1.0){
const now = audio_ctx.currentTime;
const duration = 0.22; // Pulso de sirena industrial rápido
const preset_bus = audio_ctx.createGain();
preset_bus.gain.setValueAtTime(volume_multiplier * 0.45, now);
// 1. REVERBERACIÓN CORTA DE MAMPARA (Eco de pasillo de nave)
const room_delay = audio_ctx.createDelay();
room_delay.delayTime.setValueAtTime(0.045, now);
const room_feedback = audio_ctx.createGain();
room_feedback.gain.setValueAtTime(0.35, now);
const room_filter = audio_ctx.createBiquadFilter();
room_filter.type = "lowpass";
room_filter.frequency.setValueAtTime(1200, now);
room_delay.connect(room_filter);
room_filter.connect(room_feedback);
room_feedback.connect(room_delay);
room_filter.connect(preset_bus);
// 2. ENVOLVENTE DINÁMICA DE BOCINA (Ataque percusivo + cuerpo)
const env = audio_ctx.createGain();
env.gain.setValueAtTime(0.001, now);
// Ataque rápido simulando apertura de válvula
env.gain.linearRampToValueAtTime(0.85, now + 0.02);
env.gain.setValueAtTime(0.85, now + 0.14);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
// 3. GENERADOR DE SIRENA INDUSTRIAL (Síntesis FM limpia)
// Transportadora principal: Tono puro de bocina (senoidal limpia)
const carrier = audio_ctx.createOscillator();
carrier.type = "sine";
// Barrido característico de evacuación: empieza arriba y resbala hacia abajo
carrier.frequency.setValueAtTime(740, now);
carrier.frequency.exponentialRampToValueAtTime(460, now + duration);
// Modulador FM: Añade el zumbido de vibración de la membrana metálica
const modulator = audio_ctx.createOscillator();
const mod_gain = audio_ctx.createGain();
modulator.type = "sine";
// Modulación a 45Hz para simular el traqueteo del motor de la sirena
modulator.frequency.setValueAtTime(45, now);
mod_gain.gain.setValueAtTime(140, now);
mod_gain.gain.exponentialRampToValueAtTime(40, now + duration);
modulator.connect(mod_gain);
mod_gain.connect(carrier.frequency);
// 4. SEGUNDO TONO ARMÓNICO (Cuerpo de resonador hueco)
const sub_carrier = audio_ctx.createOscillator();
const sub_gain = audio_ctx.createGain();
sub_carrier.type = "triangle";
sub_carrier.frequency.setValueAtTime(370, now);
sub_carrier.frequency.exponentialRampToValueAtTime(230, now + duration);
sub_gain.gain.setValueAtTime(0.3, now);
sub_carrier.connect(sub_gain);
sub_gain.connect(env);
// 5. FILTRO PASO-BANDA (Emula el megáfono físico de cabina)
const megaphone_filter = audio_ctx.createBiquadFilter();
megaphone_filter.type = "bandpass";
megaphone_filter.frequency.setValueAtTime(650, now);
megaphone_filter.Q.setValueAtTime(1.8, now);
carrier.connect(env);
env.connect(megaphone_filter);
// Enviamos al bus directo y una copia a la reverberación de mampara
megaphone_filter.connect(preset_bus);
megaphone_filter.connect(room_delay);
preset_bus.connect(output_node);
// Disparo coordinado
modulator.start(now);
carrier.start(now);
sub_carrier.start(now);
modulator.stop(now + duration);
carrier.stop(now + duration);
sub_carrier.stop(now + duration);
};
})();

View File

@ -0,0 +1,327 @@
"use strict";
const BehaviorStrategies = (function(){
const STRATEGIES = {
// 1. PERSECUCIÓN DEL JUGADOR CON ROTACIÓN Y ACELERACIÓN SUAVES
track_drone : (enemy, delta_time, drone_info) => {
if(drone_info){
const dx = drone_info.cell_x - enemy.x;
const dy = drone_info.cell_y - enemy.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const accel = enemy.config.accel || 3.5;
if(dist > 0.1){
enemy.vx += (dx / dist) * accel * delta_time;
enemy.vy += (dy / dist) * accel * delta_time;
}
const max_speed = enemy.config.max_speed || 5.2;
const spd = Math.sqrt(enemy.vx * enemy.vx + enemy.vy * enemy.vy);
if(spd > max_speed){
enemy.vx = (enemy.vx / spd) * max_speed;
enemy.vy = (enemy.vy / spd) * max_speed;
}
}
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
// ROTACIÓN PROGRESIVA Y SUAVE (Giro de timón realista)
const speed_sq = enemy.vx * enemy.vx + enemy.vy * enemy.vy;
if(speed_sq > 0.05){
const target_angle = Math.atan2(enemy.vy, enemy.vx) + (Math.PI / 2);
let diff = target_angle - enemy.angle;
while(diff < -Math.PI) diff += Math.PI * 2;
while(diff > Math.PI) diff -= Math.PI * 2;
const turn_speed = enemy.config.turn_speed || 4.2; // Radianes/segundo
const max_step = turn_speed * delta_time;
if(Math.abs(diff) <= max_step){
enemy.angle = target_angle;
} else {
enemy.angle += Math.sign(diff) * max_step;
}
}
},
// 2. ESTÁTICO CON ROTACIÓN Y DERIVA INERCIAL
static : (enemy, delta_time) => {
enemy.angle += 0.5 * delta_time;
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
enemy.vx *= 0.98;
enemy.vy *= 0.98;
},
// 3. REBOTE EN LOS MÁRGENES DE LA CABINA
// 3. PATRULLA CON FRENADO EN BORDES, GIRO SUAVE Y ACELERACIÓN
bounce_screen : (enemy, delta_time) => {
// Inicialización de estado interno de maniobra
if(!enemy.maneuver_state){
enemy.maneuver_state = {
phase : "cruising", // "cruising" | "braking_and_turning" | "accelerating"
target_vx : enemy.vx || 3.5,
target_vy : enemy.vy || 3.5,
target_angle : enemy.angle || 0,
cruise_speed : Math.sqrt((enemy.vx * enemy.vx) + (enemy.vy * enemy.vy)) || 4.2
};
}
const m = enemy.maneuver_state;
const boundary_limit = 15.5; // Distancia límite en celdas desde el centro
const turn_speed = enemy.config.turn_speed || 4.5; // Velocidad de giro en rad/s
// FASE 1: VUELO DE CRUCERO
if(m.phase === "cruising"){
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
// Comprobamos si rebasa el margen perimetral
const hit_x = (enemy.x > boundary_limit && enemy.vx > 0) || (enemy.x < -boundary_limit && enemy.vx < 0);
const hit_y = (enemy.y > boundary_limit && enemy.vy > 0) || (enemy.y < -boundary_limit && enemy.vy < 0);
if(hit_x || hit_y){
// Calculamos la nueva dirección vectorial reflejada
const new_dir_x = hit_x ? -Math.sign(enemy.vx) : (enemy.vx > 0 ? 1 : -1);
const new_dir_y = hit_y ? -Math.sign(enemy.vy) : (enemy.vy > 0 ? 1 : -1);
// Pequeña variación angular aleatoria para que no rebote siempre en el mismo bucle
const jitter = (Math.random() - 0.5) * 0.4;
const base_angle = Math.atan2(new_dir_y, new_dir_x) + jitter;
m.target_vx = Math.cos(base_angle) * m.cruise_speed;
m.target_vy = Math.sin(base_angle) * m.cruise_speed;
m.target_angle = base_angle + (Math.PI / 2);
m.phase = "braking_and_turning";
} else {
// Mantiene la orientación alineada con la dirección de avance
if(Math.abs(enemy.vx) > 0.05 || Math.abs(enemy.vy) > 0.05){
enemy.angle = Math.atan2(enemy.vy, enemy.vx) + (Math.PI / 2);
}
}
}
// FASE 2: FRENADO Y GIRO SUAVE EN EL MARGEN
else if(m.phase === "braking_and_turning"){
// Desaceleración progresiva (retrocohetes)
enemy.vx *= Math.pow(0.08, delta_time);
enemy.vy *= Math.pow(0.08, delta_time);
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
// Viraje suave hacia el nuevo ángulo objetivo
let diff = m.target_angle - enemy.angle;
while(diff < -Math.PI) diff += Math.PI * 2;
while(diff > Math.PI) diff -= Math.PI * 2;
const max_step = turn_speed * delta_time;
if(Math.abs(diff) <= max_step){
enemy.angle = m.target_angle;
} else {
enemy.angle += Math.sign(diff) * max_step;
}
const current_speed = Math.sqrt((enemy.vx * enemy.vx) + (enemy.vy * enemy.vy));
// Si ha frenado a casi cero y ya completó el giro, iniciamos aceleración
if(current_speed < 0.35 && Math.abs(diff) < 0.25){
enemy.vx = 0;
enemy.vy = 0;
m.phase = "accelerating";
}
}
// FASE 3: ACELERACIÓN HACIA LA NUEVA DIRECCIÓN
else if(m.phase === "accelerating"){
const forward_angle = enemy.angle - (Math.PI / 2);
const accel_rate = enemy.config.accel || 5.0; // Tasa de aceleración
enemy.vx += Math.cos(forward_angle) * accel_rate * delta_time;
enemy.vy += Math.sin(forward_angle) * accel_rate * delta_time;
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
const current_speed = Math.sqrt((enemy.vx * enemy.vx) + (enemy.vy * enemy.vy));
// Al alcanzar la velocidad de crucero, vuelve al ciclo normal
if(current_speed >= m.cruise_speed){
const factor = m.cruise_speed / current_speed;
enemy.vx *= factor;
enemy.vy *= factor;
m.phase = "cruising";
}
}
},
// 4. BALÍSTICO / CRUCE DIRECTO
cross_screen : (enemy, delta_time) => {
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
enemy.angle = Math.atan2(enemy.vy, enemy.vx) + (Math.PI / 2);
const dist_center = Math.sqrt(enemy.x * enemy.x + enemy.y * enemy.y);
if(enemy.age > 1.5 && dist_center > 24.0){
enemy.alive = false;
}
},
// 5. IA LANZADOR DE ASTEROIDES (Confinado en el área jugable)
thrower_ai : (enemy, delta_time, drone_info, all_enemies) => {
const turn_speed = enemy.config.turn_speed || 4.2;
const cruise_speed = enemy.config.max_speed || 4.8;
const flee_distance = enemy.config.flee_distance || 7.5;
const max_patrol_radius = 16.5; // Radio límite donde el Dron siempre puede alcanzarlo
let target_vx = enemy.vx;
let target_vy = enemy.vy;
// 1. EVALUAR AMENAZA DEL DRON (Prioridad 1: Huir sin salirse del mapa)
let is_fleeing = false;
if(drone_info){
const dx_drone = enemy.x - drone_info.cell_x;
const dy_drone = enemy.y - drone_info.cell_y;
const dist_drone = Math.sqrt(dx_drone * dx_drone + dy_drone * dy_drone);
if(dist_drone < flee_distance && dist_drone > 0.05){
is_fleeing = true;
// Dirección de alejamiento puro
let flee_x = dx_drone / dist_drone;
let flee_y = dy_drone / dist_drone;
// Si está cerca del borde, no puede seguir retrocediendo: se desvía en ángulo tangente
const dist_center = Math.sqrt(enemy.x * enemy.x + enemy.y * enemy.y);
if(dist_center > 13.5){
// Vector hacia el centro
const to_center_x = -enemy.x / dist_center;
const to_center_y = -enemy.y / dist_center;
// Mezcla huida con escape lateral (tangente) para rodear al dron hacia el centro
const tangent_x = -flee_y;
const tangent_y = flee_x;
// Elegir el sentido tangencial que más se acerque al centro
const dot = (tangent_x * to_center_x) + (tangent_y * to_center_y);
const side = dot >= 0 ? 1 : -1;
flee_x = (flee_x * 0.3) + (tangent_x * side * 0.7) + (to_center_x * 0.5);
flee_y = (flee_y * 0.3) + (tangent_y * side * 0.7) + (to_center_y * 0.5);
const norm = Math.sqrt(flee_x * flee_x + flee_y * flee_y) || 1;
flee_x /= norm;
flee_y /= norm;
}
target_vx = flee_x * cruise_speed * 1.2;
target_vy = flee_y * cruise_speed * 1.2;
}
}
// 2. SI NO ESTÁ HUYENDO: BUSCAR Y DISPARAR ASTEROIDES
if(!is_fleeing){
let closest_ast = null;
let min_ast_dist = Infinity;
if(Array.isArray(all_enemies)){
for(let i = 0; i < all_enemies.length; i++){
const other = all_enemies[i];
if(other && other.is_alive() && other.is_asteroid && !other.is_targeted_towards_mothership()){
const st = other.get_state();
// Ignora asteroides que ya se hayan salido
const d_center = Math.sqrt(st.x * st.x + st.y * st.y);
if(d_center > 18.5) continue;
const dx = st.x - enemy.x;
const dy = st.y - enemy.y;
const d = Math.sqrt(dx * dx + dy * dy);
if(d < min_ast_dist){
min_ast_dist = d;
closest_ast = other;
}
}
}
}
if(closest_ast){
const ast_st = closest_ast.get_state();
const dx = ast_st.x - enemy.x;
const dy = ast_st.y - enemy.y;
// Si está pegado al asteroide: lo dispara hacia el centro
if(min_ast_dist < 2.2){
const to_center_dist = Math.sqrt(ast_st.x * ast_st.x + ast_st.y * ast_st.y);
if(to_center_dist > 0.1){
const launch_speed = 7.5;
const launch_vx = -(ast_st.x / to_center_dist) * launch_speed;
const launch_vy = -(ast_st.y / to_center_dist) * launch_speed;
closest_ast.launch_towards_mothership(launch_vx, launch_vy);
}
// Rebote inercial tras el tiro
target_vx = (dx / (min_ast_dist || 1)) * -cruise_speed;
target_vy = (dy / (min_ast_dist || 1)) * -cruise_speed;
} else {
target_vx = (dx / min_ast_dist) * cruise_speed;
target_vy = (dy / min_ast_dist) * cruise_speed;
}
} else {
// Si no hay asteroides y está muy lejos, patrulla de vuelta hacia el radio seguro
const cur_dist = Math.sqrt(enemy.x * enemy.x + enemy.y * enemy.y);
if(cur_dist > 14.0){
target_vx = (-enemy.x / cur_dist) * (cruise_speed * 0.7);
target_vy = (-enemy.y / cur_dist) * (cruise_speed * 0.7);
} else {
enemy.angle += 0.8 * delta_time;
}
}
}
// Aplicación de aceleración
const accel = 4.2;
enemy.vx += (target_vx - enemy.vx) * Math.min(1.0, accel * delta_time);
enemy.vy += (target_vy - enemy.vy) * Math.min(1.0, accel * delta_time);
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
// Freno y clamp perimetral absoluto: el lanzador NUNCA puede rebasar las 17.0 celdas
const dist_edge = Math.sqrt(enemy.x * enemy.x + enemy.y * enemy.y);
if(dist_edge > max_patrol_radius){
const factor = max_patrol_radius / dist_edge;
enemy.x *= factor;
enemy.y *= factor;
// Anular componente de velocidad que lo empuja hacia afuera
const dot_out = (enemy.vx * (enemy.x / max_patrol_radius)) + (enemy.vy * (enemy.y / max_patrol_radius));
if(dot_out > 0){
enemy.vx -= dot_out * (enemy.x / max_patrol_radius);
enemy.vy -= dot_out * (enemy.y / max_patrol_radius);
}
}
// Giro hacia donde avanza
const spd = Math.sqrt(enemy.vx * enemy.vx + enemy.vy * enemy.vy);
if(spd > 0.1){
const target_angle = Math.atan2(enemy.vy, enemy.vx) + (Math.PI / 2);
let diff = target_angle - enemy.angle;
while(diff < -Math.PI) diff += Math.PI * 2;
while(diff > Math.PI) diff -= Math.PI * 2;
const max_step = turn_speed * delta_time;
enemy.angle += Math.sign(diff) * Math.min(Math.abs(diff), max_step);
}
},
};
return {
get : (name) => STRATEGIES[name] || STRATEGIES.static,
register : (name, strategy) => { STRATEGIES[name] = strategy; }
};
})();

View File

@ -0,0 +1,153 @@
"use strict";
const SpawnStrategies = (function(){
const STRATEGIES = {
// 1. SOBREVUELO RASANTE CON EASE-OUT CÚBICO
overflight : {
init : (enemy, config, xgravity) => {
const base_size = config.size || 1.8;
const duration = (typeof config.duration === "number")
? config.duration
: xgravity.settings.get("enemy_overflight_duration", null, 1.4);
const initial_scale = (typeof config.initial_scale === "number")
? config.initial_scale
: xgravity.settings.get("enemy_overflight_initial_scale", null, 6.5);
const target_angle = Math.random() * Math.PI * 2;
const target_dist = 4.0 + (Math.random() * 7.0);
const target_x = Math.cos(target_angle) * target_dist;
const target_y = Math.sin(target_angle) * target_dist;
const spawn_angle = Math.random() * Math.PI * 2;
const giant_half_size = (base_size * initial_scale) / 2;
const spawn_distance = 22.0 + giant_half_size + 2.0;
const spawn_x = Math.cos(spawn_angle) * spawn_distance;
const spawn_y = Math.sin(spawn_angle) * spawn_distance;
enemy.spawn_data = {
spawn_x, spawn_y,
target_x, target_y,
duration : Math.max(0.1, duration),
initial_scale
};
enemy.x = spawn_x;
enemy.y = spawn_y;
enemy.scale = initial_scale;
enemy.alpha = 0.4;
enemy.ready = false;
const dx = target_x - spawn_x;
const dy = target_y - spawn_y;
enemy.angle = Math.atan2(dy, dx) + (Math.PI / 2);
},
update : (enemy, delta_time) => {
const data = enemy.spawn_data;
const t = Math.min(1.0, enemy.age / data.duration);
const inv_t = 1.0 - t;
const ease_progress = 1.0 - (inv_t * inv_t * inv_t);
enemy.x = data.spawn_x + ((data.target_x - data.spawn_x) * ease_progress);
enemy.y = data.spawn_y + ((data.target_y - data.spawn_y) * ease_progress);
const speed_factor = (3.0 * inv_t * inv_t) / data.duration;
enemy.vx = (data.target_x - data.spawn_x) * speed_factor;
enemy.vy = (data.target_y - data.spawn_y) * speed_factor;
enemy.scale = data.initial_scale - ((data.initial_scale - 1.0) * ease_progress);
enemy.alpha = 0.4 + (ease_progress * 0.6);
if(t >= 1.0){
enemy.scale = 1.0;
enemy.alpha = 1.0;
enemy.ready = true;
const spd = Math.sqrt(enemy.vx * enemy.vx + enemy.vy * enemy.vy);
if(spd > 0.001){
enemy.vx = (enemy.vx / spd) * 3.2;
enemy.vy = (enemy.vy / spd) * 3.2;
}
}
}
},
// 2. EMERGENCIA DESDE EL FONDO ESPACIAL (DEEP SPACE)
deep_space : {
init : (enemy, config) => {
const spawn_angle = Math.random() * Math.PI * 2;
const r = Math.random() * 15.0;
enemy.x = Math.cos(spawn_angle) * r;
enemy.y = Math.sin(spawn_angle) * r;
enemy.scale = 0.0;
enemy.alpha = 0.3;
enemy.ready = false;
enemy.scale_speed = config.scale_speed || 0.65;
enemy.angle = Math.random() * Math.PI * 2;
},
update : (enemy, delta_time) => {
enemy.scale += enemy.scale_speed * delta_time;
enemy.alpha = 0.3 + (enemy.scale * 0.7);
if(enemy.scale >= 1.0){
enemy.scale = 1.0;
enemy.alpha = 1.0;
enemy.ready = true;
}
}
},
// 3. ENTRADA PERIMETRAL EXTERIOR (100% FUERA DE PANTALLA)
perimeter : {
init : (enemy, config) => {
const base_size = config.size || 1.8;
const spawn_angle = Math.random() * Math.PI * 2;
// Calculado holgadamente para que el sprite nunca corte el borde al nacer
const spawn_distance = 22.0 + (base_size / 2) + 2.0;
enemy.x = Math.cos(spawn_angle) * spawn_distance;
enemy.y = Math.sin(spawn_angle) * spawn_distance;
// Apunta hacia el cuadrante interior visible con ligera dispersión
const target_angle = spawn_angle + Math.PI + ((Math.random() - 0.5) * 0.7);
const speed = config.initial_speed || 4.2;
enemy.vx = Math.cos(target_angle) * speed;
enemy.vy = Math.sin(target_angle) * speed;
enemy.scale = 1.0;
enemy.alpha = 1.0;
enemy.ready = false;
enemy.angle = Math.atan2(enemy.vy, enemy.vx) + (Math.PI / 2);
},
update : (enemy, delta_time) => {
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
// Al entrar en el radio de combate (16 celdas), se activa para combate
const dist = Math.sqrt(enemy.x * enemy.x + enemy.y * enemy.y);
if(dist <= 16.0){
enemy.ready = true;
}
}
}
};
return {
get : (mode) => {
// Si le pasamos un array de modos, elige uno aleatoriamente
if(Array.isArray(mode)){
const pick = mode[Math.floor(Math.random() * mode.length)];
return STRATEGIES[pick] || STRATEGIES.overflight;
}
return STRATEGIES[mode] || STRATEGIES.overflight;
},
register : (name, strategy) => { STRATEGIES[name] = strategy; }
};
})();

View File

@ -0,0 +1,200 @@
"use strict";
const Common = (function(){
const Common = function(){};
Common.get_keys = (...items) => items.reduce((keys, item) => {
if(item){
if(typeof item == "string" && /^[a-z_][a-z0-9_]*$/i.test(item) && !keys.includes(item))
keys.push(item);
else if(item instanceof Array)
for(const key of Common.get_keys(...item))
keys.includes(key) || keys.push(key);
};
return keys;
}, []);
Common.get_dictionaries = (...items) => items.reduce((dictionaries, item) => {
if(item){
if(item.constructor == Object)
dictionaries.push(item);
else if(item instanceof Array)
dictionaries.push(...Common.get_dictionaries(...item));
};
return dictionaries;
}, []);
Common.get_value = (keys, inputs, _default = null) => {
if((keys = Common.get_keys(keys)).length)
for(const subinputs of Common.get_dictionaries(inputs))
for(const key of keys)
if(subinputs[key] !== undefined)
return subinputs[key];
return _default;
};
Common.get_dictionary = (items, overwrite = false) => {
if(items){
if(items.constructor == Object)
return items;
else if(items instanceof Array)
return items.reduce((dictionary, item) => {
Object.entries(Common.get_dictionary(item)).forEach(([key, value]) => {
if(overwrite || dictionary[key] === undefined)
dictionary[key] = value;
});
return dictionary;
}, {});
};
return {};
};
Common.string_variables = (string, variables, _default = null) => {
variables = Common.get_dictionary(variables);
return ("" + string).replace(/\{([^\{\}]+)\}/g, (all, key) => (
variables[key] !== undefined ? variables[key] :
_default !== null ? _default :
all));
};
Common.execute_array = (array, each_callback, end_callback, i = 0) => {
if(i == array.length)
end_callback();
else
each_callback(array[i], () => {
Common.execute_array(array, each_callback, end_callback, i + 1);
});
};
Common.SCSS = (data, selector = "") => {
/** @type {Arrya.<string>} */
const childs = [];
/** @type {string} */
let css = ``;
Object.entries(data).forEach(([key, value]) => {
if(value && value.constructor == Object)
key.split(",").forEach(key => {
/** @type {string} */
const subselector = `${(selector ? selector + (
key[0] == "&" ? "" : " "
) : "") + (
key[0] == "&" ? key.substring(1) : key
)}`,
/** @type {Array<string>} */
subcss = Common.SCSS(value, subselector);
childs.push(`\n\n${subselector}{${subcss[0]}\n}`, ...subcss.slice(1));
});
else
css += `\n ${key.replace(/[^a-z0-9]+/gi, "-")} : ${value === null ? "none" : value};`;
});
if(!selector){
/** @type {HTMLStyleElement} */
let style = document.querySelector("head>style");
if(!style){
style = document.querySelector("head").appendChild(document.createElement("style"));
[
["data-type", "text/css;charset=utf-8"],
["data-language", "CSS3"],
["charset", "utf-8"]
].forEach(([key, value]) => style.setAttribute(key, value === null ? "none" : value));
};
style.appendChild(document.createTextNode(css + childs.join("")));
};
return [css, ...childs];
};
/**
* @param {!string} target
* @param {!string} query
* @returns {boolean}
*/
Common.match_query = function(target, query){
if(!query || !query.trim())
return true;
const raw_query = query.trim(),
regex_match = raw_query.match(/^\/(.+)\/([gimsuy]*)$/);
if(!target)
target = "";
if(regex_match)
try{
const pattern = regex_match[1],
flags = regex_match[2] || "i",
regular_expression = new RegExp(pattern, flags);
return regular_expression.test(target);
}catch(exception){
return false;
};
const target_lower = ("" + target).toLowerCase(),
or_clauses = raw_query.split("||").map(s => s.trim()).filter(Boolean);
if(!or_clauses.length)
return true;
return or_clauses.some(or_clause => {
const and_terms = or_clause.split("&&").map(s => s.trim()).filter(Boolean);
if(!and_terms.length)
return true;
return and_terms.every(term => {
const is_negated = term.startsWith("!"),
clean_term = (is_negated ? term.slice(1) : term).trim().toLowerCase();
if(!clean_term)
return true;
const matches = target_lower.includes(clean_term);
return is_negated ? !matches : matches;
});
});
};
Common.get_minutes_diff = (start_time, end_time) => {
if(!start_time || !end_time || start_time === "--:--" || end_time === "--:--")
return null;
const [sh, sm] = start_time.split(":").map(Number),
[eh, em] = end_time.split(":").map(Number);
if(isNaN(sh) || isNaN(sm) || isNaN(eh) || isNaN(em))
return null;
let start_mins = sh * 60 + sm,
end_mins = eh * 60 + em;
// Si cruza medianoche (ej. 23:50 a 00:15)
if(end_mins < start_mins)
end_mins += 24 * 60;
return end_mins - start_mins;
};
return Common;
})();

View File

@ -0,0 +1,78 @@
"use strict";
/** @type {Dictionary.<string, Dictionary.<string, string>>} */
const i18n = {
espanol : {
xgravity : "XGravity",
// Botones y navegación
btn_play : "Iniciar partida",
btn_options : "Opciones",
btn_ranking : "Ranking",
btn_credits : "Créditos",
btn_back : "Volver",
btn_resume : "Reanudar",
btn_exit : "Salir al menú",
btn_press_key : "Presiona una tecla...",
btn_new_game: "Nueva Partida",
// Opciones
opt_title : "Opciones",
opt_canvas_quality : "Calidad del Canvas ({quality}%)",
opt_action_key : "Tecla de acción",
opt_background : "Fondo espacial",
opt_bg_image : "Imagen fija (Retro 2004)",
opt_bg_stars : "Campo de estrellas (Dinámico)",
opt_bg_procedural : "Sector procedural (Avanzado)",
opt_difficulty : "Dificultad",
opt_show_fps : "Mostrar FPS",
// Niveles de dificultad
diff_training : "Entrenamiento",
diff_newbie : "Novato",
diff_lazy : "Vago",
diff_average : "Promedio",
diff_hard : "Durillo",
diff_berserk : "Engorilado",
diff_insane : "Insano",
// Ranking
rank_title : "Ranking",
rank_empty : "Aún no hay puntuaciones registradas.",
rank_col_rank : "#",
rank_col_name : "Piloto",
rank_col_score : "Puntos",
// Créditos
cred_title : "Créditos",
cred_original_concept : "Concepto original y programación (2004):",
cred_author : "KyMAN",
cred_original_tech : "Desarrollado originalmente en ActionScript 2.0 para Macromedia Flash 8 (obsoleto).",
cred_old_urls : "Alojado originalmente en Tripod Lycos (https://tripod.lycos.es/miglug/xgravity) y posteriormente en ATSpace.",
cred_remake : "Revival web actual (HTML5 / ES6):",
cred_remake_authors : "KyMAN & Gemini (Google AI)",
cred_assets : "Recursos y Arte:",
cred_bg_photo : "Fondo espacial: Fotografía por Vincentiu Solomon en Unsplash (Licencia libre Unsplash).",
cred_tatermand_art : "Set espacial y planetas: Space Game Art Pack (Extended) por Tatermand en OpenGameArt.org (Licencia CC-BY-SA 3.0).",
cred_links : "Enlaces:",
cred_web : "Web oficial: {url}",
cred_git : "Repositorio: {url}",
// HUD & Partida
hud_score : "Puntos: {score}",
hud_shield : "Escudo:",
hud_pause_title : "Pausa",
// Game Over
game_over_title : "MISIÓN FALLIDA",
btn_retry : "REINTENTAR",
btn_main_menu : "MENÚ PRINCIPAL",
stat_final_score : "PUNTUACIÓN",
stat_difficulty : "DIFICULTAD",
stat_enemies_spawned : "CONTACTOS DETECTADOS",
stat_enemies_destroyed : "TOTAL ELIMINADOS",
stat_shield_kills : "BAJAS POR ESCUDO",
stat_clear_rate : "EFECTIVIDAD TÁCTICA"
}
};

View File

@ -0,0 +1,390 @@
"use strict";
/** @type {Dictionary.<string, any|null>} */
const settings = {
cells : 40,
position : "body",
class_name : "xgravity",
frames_per_second : 12,
language : "espanol",
show_fps : true,
// Configuraciones de juego y pantalla
canvas_quality : 100,
action_key : "Space",
background_mode : "procedural", // "image" | "stars" | "procedural"
// Parámetros para fondo con imagen ("image")
// bg_image_url : "https://images.unsplash.com/photo-1506703719100-a0f3a48c0f86?q=80&w=1600&auto=format&fit=crop",
bg_image_url : "images/backgrounds/photo-1506703719100-a0f3a48c0f86.jpeg",
bg_image_pan_speed_x : 5, // px por segundo (0 para totalmente estático)
bg_image_pan_speed_y : 2,
// Parámetros para el generador de estrellas ("stars")
bg_stars_count : 140,
bg_stars_size_min : 0.5,
bg_stars_size_max : 2.2,
bg_stars_speed_min : 0.1,
bg_stars_speed_max : 0.6,
bg_stars_dir_x : 0, // -1 (izq) a 1 (der)
bg_stars_dir_y : 1, // -1 (arriba) a 1 (abajo)
bg_stars_color : "rgba(186, 230, 253, {alpha})",
// Paŕametros para fondo dinámico.
bg_procedural_far_stars : {
probablity : .96,
size : [.2, .7],
images : [
"images/game/Far-Star-1.webp",
"images/game/Far-Star-2.webp",
"images/game/Far-Star-3.webp"
]
},
bg_procedural_nebulas : {
probablity : .87,
size : [3, 60],
images : [
"images/game/Blue-Dust.webp",
"images/game/Cold-Nebula.webp",
"images/game/Dust.webp",
"images/game/Grey-Asteroid-Dust.webp",
"images/game/Hot-Nebula.webp",
"images/game/Violet-Dust.webp",
"images/game/Yellow-Dust.webp"
]
},
bg_procedural_near_stars : {
probablity : .21,
size : [.5, 1.7],
images : [
"images/game/Layer.webp",
"images/game/Layer2.webp"
],
},
bg_procedural_suns : {
probablity : .32,
timer : [2, 10],
size : [10, 50],
images : [
"images/game/Sun.fixed.webp",
"images/game/Red-Giant.webp"
],
},
bg_procedural_planets : {
probablity : .17,
size : [7, 30],
images : [
"images/game/Ice-Giant.webp",
"images/game/Gas-Giant.webp",
"images/game/Exoplanet3.webp",
"images/game/Exoplanet2.webp",
"images/game/Exoplanet.webp"
]
},
// Configuración del Dron / Nave
drone_image : "images/game/Louse.webp",
drone_size : 2.5, // Tamaño en celdas (ancho/alto en base al grid de 40 celdas)
drone_gravity : 2.2, // Multiplicador de fuerza gravitatoria hacia el cursor
drone_friction : 0.985, // Fricción / inercia en el vacío (0 = freno total, 1 = inercia infinita)
// drone_max_speed : 25, // Velocidad máxima de seguridad en celdas/segundo
// drone_force : 1.2, // Multiplicador de fuerza f
// drone_max_speed : 28, // Límite máximo en celdas/segundo
// Ajustes de física del dron
drone_force : 3, // Multiplicador fx y fy
drone_max_accel : 35, // Tope del acumulador ax y ay
// Configuración del Escudo Activo
drone_shield_drain : 25, // Consumo por segundo al mantener pulsado (0 a 100)
drone_shield_recovery : 12, // Recuperación por segundo en reposo
drone_shield_radius : 1.7, // Radio del escudo en celdas (alrededor del dron)
// Alerta de Escudo Bajo en el Dron
// drone_shield_low_threshold : 25, // Porcentaje a partir del cual salta la alarma (<= 25%)
// drone_shield_alarm_interval : 0.75, // Intervalo de repetición del sonido de alarma (segundos)
// drone_shield_beacon_speed : 6.0, // Velocidad de giro del girofaro (radianes/segundo)
// Alarma de Escudo Bajo estilo Alien (Klaxon)
drone_shield_low_threshold : 25, // Porcentaje crítico (<= 25%)
// drone_shield_alarm_interval : 0.22, // Cadencia rápida de repetición (segundos entre pulsos)
// drone_shield_beacon_speed : 9.5, // Girofaro acelerado al ritmo de la alarma
drone_shield_alarm_interval : 0.26, // Intervalo ajustado a la longitud del pulso
drone_shield_beacon_speed : 8.5, // Giro acompasado
// Curvas dinámicas de Alerta de Perímetro / Salida de Zona
drone_perimeter_safe_radius : 15.0, // Radio donde empieza la advertencia (celdas)
drone_perimeter_critical_radius : 18.5, // Radio límite antes de perder la nave (celdas)
drone_perimeter_alarm_interval_max : 0.7, // Intervalo al rozar las 15 celdas
drone_perimeter_alarm_interval_min : 0.2, // Intervalo al borde del límite fatal
// Gestión del Escudo del Dron
drone_shield_recharge_time : 20.0, // Segundos que tarda en recargar de 0 a 100%
drone_shield_kill_reward : 25.0, // Puntos de escudo recuperados al destruir un enemigo con el escudo
drone_shield_drain : 25.0, // Consumo de escudo por segundo mientras está activo
// Configuración de Enemigos
enemy_spawn_rate : 1.6, // Frecuencia de aparición (en segundos)
enemy_speed : 5.0, // Velocidad lineal en celdas por segundo
enemy_size : 2.2, // Tamaño en celdas
enemy_spawn_radius : 22.0, // Distancia radial de generación
enemy_score_value : 100, // Puntos al destruir un enemigo con el escudo
// Curva de dificultad y desbloqueo progresivo
enemy_spawn_interval : 0.1, // Frecuencia con la que se tira el dado (en segundos)
enemy_time_to_max_difficulty : 120, // Segundos hasta alcanzar el 100% del rango (0.11)
enemy_min_threshold : 0.04, // Umbral inicial (solo estáticos al arrancar)
enemy_max_threshold : 0.11, // Umbral máximo accesible
// Configuración de Entrada Táctica y Sobrevuelo
enemy_overflight_initial_scale : 120.0, // Escala inicial al sobrevolar desde atrás (ajústalo a tu gusto: 4.0, 6.0, 8.0...)
enemy_overflight_scale_speed : 1.8, // Velocidad con la que desciende hacia el plano de juego
// Duración de la retícula de triangulación táctica al aparecer cualquier enemigo (en segundos)
enemy_target_lock_duration : 1.2,
// Configuración de HUD de combate sobre enemigos
enemy_show_health_bars : true, // Activar/desactivar barras de vida
enemy_health_bar_width : 1.8, // Ancho de la barra en celdas
enemy_damage_popup_duration : 0.75, // Segundos que dura el texto flotante de daño
// Ajustes de sobrevuelo
enemy_overflight_initial_scale : 26.5, // Escala inicial
enemy_overflight_duration : 1.4, // Segundos exactos que tarda en alcanzar el plano 2D
// Configuración general de sobrevuelo
// Plantilla de enemigos
enemies_roster : [
{
id: "static_mine",
// range: [0.00, 0.04],
range: [0.07, 0.09],
image: "images/game/Cricket-Snipper.webp",
size: 2.0,
mass: 5.0, // Pesada, se mueve poco al impactar
health: 40, // Aguanta un toque suave o revienta si entras a toda velocidad
spawn_mode: "deep_space", // Crece de 0 a 1
behavior: "static",
points: 50
},
{
id: "bouncer",
range: [0.04, 0.07],
image: "images/game/Mantis-Hull.webp",
size: 2.2,
mass: 2.5, // Masa media, rebota bien
health: 60,
spawn_mode: "perimeter", // Entra desde el exterior
behavior: "bounce_screen", // Rebota en el área [-23, 23]
accel: 5.5, // Potencia con la que acelera al salir del frenado
turn_speed: 4.8, // Rapidez con la que vira su timón al borde
points: 100
},
{
id: "chaser",
// range: [0.07, 0.09],
range: [0.00, 0.04],
image: "images/game/Scarab-Hull.webp",
size: 1.8,
mass: 1.8, // Ligera y ágil
health: 30, // Frágil si la embistes con escudo
spawn_mode: ["overflight", "deep_space", "perimeter"],
behavior: "track_drone",
points: 150
},
{
id: "rusher",
range: [0.09, 0.11],
image: "images/game/Strange-Body.webp",
size: 1.5,
mass: 4.0, // Densidad alta: impacto como un misil
health: 20,
spawn_mode: "perimeter",
behavior: "cross_screen", // Cruza a toda mecha
points: 200
},
{
id: "asteroid",
range: [0.12, 0.16],
// Array de imágenes aleatorias:
image: [
"images/game/Grey-Asteroid-1.webp",
"images/game/Grey-Asteroid-2.webp",
"images/game/Grey-Asteroid-3.webp"
],
size: 2.4,
mass: 7.0,
health: 50,
spawn_mode: "perimeter",
behavior: "cross_screen",
mothership_damage: 25, // Daño directo base al casco
points: 80
},
{
id: "asteroid_thrower",
range: [0.16, 0.19],
image: [
"images/game/Locust-Hull.webp"
],
size: 2.0,
mass: 2.2,
health: 45,
spawn_mode: ["perimeter", "overflight"],
behavior: "thrower_ai",
turn_speed: 4.5,
max_speed: 4.8,
flee_distance: 7.5,
points: 250
}
],
// Configuración de Efectos de Explosión
explosion_particles_range : [45, 75], // Número aleatorio de partículas [mínimo, máximo]
explosion_particles_spread : 31.0, // Radio/fuerza de alcance de dispersión (celdas/s)
explosion_shockwave_radius : 14.5, // Radio máximo de la onda expansiva en celdas
explosion_volume : 3, // Volumen general de las explosiones (0.0 a 1.0)
// Balanceo dinámico de la cúpula e inercia de cabina
cockpit_drift_amount : 0.6, // Máximo desplazamiento en celdas por inercia del dron
cockpit_sway_speed : 0.8, // Velocidad del balanceo pasivo de respiración
// Screen Shake (Temblor de pantalla por explosiones)
screen_shake_shield_intensity : 0.25, // Intensidad al destruir con escudo (en celdas)
screen_shake_shield_duration : 0.15, // Duración sacudida de escudo (segundos)
screen_shake_hit_intensity : 0.85, // Intensidad al recibir impacto directo (en celdas)
screen_shake_hit_duration : 0.42, // Duración sacudida crítica (segundos)
// Factor de desplazamiento parallax del fondo estelar
background_parallax_amount : 0.25, // Intensidad del deslizamiento del fondo
// ==========================================
// SISTEMA DE DIFICULTAD Y MULTIPLICADORES
// ==========================================
current_difficulty : "average",
// Valores base del motor
base_enemy_spawn_interval : 0.65, // Segundos base entre chequeos de spawn
base_enemy_min_threshold : 0.04, // Probabilidad inicial de spawn
base_enemy_max_threshold : 0.22, // Límite asintótico de spawn
base_drone_shield_max : 100.0, // Capacidad base de escudo/vida del dron
base_drone_shield_drain_rate : 12.0, // Drenaje base por segundo activo
base_drone_shield_kill_reward : 10.0, // Recarga base al destruir con escudo
base_collision_damage_factor : 12.0, // Factor cinético base de daño
// Curva de progresión temporal asintótica
difficulty_ramp_half_life : 90.0, // Segundos para el 50% de la curva
difficulty_ramp_power : 1.25, // Exponente de aceleración (1.0 = lineal, >1.0 = agresivo)
// Catálogo de presets de dificultad (claves en inglés)
difficulty_presets : {
"training" : {
id : "training",
label_i18n : "diff_training",
spawn_interval_mult : 1.4,
min_threshold_mult : 0.5,
max_threshold_mult : 0.6,
ramp_speed_mult : 0.4,
shield_capacity_mult : 1.5,
shield_drain_mult : 0.6,
shield_reward_mult : 2.0,
drone_damage_taken_mult : 0.0, // Daño cero al dron
enemy_damage_taken_mult : 2.0,
mothership_damage_mult : 0.0, // ¡Inmortal!
mothership_repair_mult : 3.0,
drone_lives: 20,
mothership_damage_mult: 0.0,
},
"newbie" : {
id : "newbie",
label_i18n : "diff_newbie",
spawn_interval_mult : 1.25,
min_threshold_mult : 0.7,
max_threshold_mult : 0.75,
ramp_speed_mult : 0.6,
shield_capacity_mult : 1.3,
shield_drain_mult : 0.8,
shield_reward_mult : 1.5,
drone_damage_taken_mult : 0.4,
enemy_damage_taken_mult : 1.4,
mothership_damage_mult : 0.5,
mothership_repair_mult : 1.8,
drone_lives: 10,
mothership_damage_mult: 0.6,
},
"lazy" : {
id : "lazy",
label_i18n : "diff_lazy",
spawn_interval_mult : 1.1,
min_threshold_mult : 0.85,
max_threshold_mult : 0.9,
ramp_speed_mult : 0.8,
shield_capacity_mult : 1.15,
shield_drain_mult : 0.9,
shield_reward_mult : 1.2,
drone_damage_taken_mult : 0.7,
enemy_damage_taken_mult : 1.2,
mothership_damage_mult : 0.75,
mothership_repair_mult : 1.3,
drone_lives: 8,
mothership_damage_mult: 0.8,
},
"average" : {
id : "average",
label_i18n : "diff_average",
spawn_interval_mult : 1.0,
min_threshold_mult : 1.0,
max_threshold_mult : 1.0,
ramp_speed_mult : 1.0,
shield_capacity_mult : 1.0,
shield_drain_mult : 1.0,
shield_reward_mult : 1.0,
drone_damage_taken_mult : 1.0,
enemy_damage_taken_mult : 1.0,
mothership_damage_mult : 1.0,
mothership_repair_mult : 1.0,
drone_lives: 5,
mothership_damage_mult: 1,
},
"hard" : {
id : "hard",
label_i18n : "diff_hard",
spawn_interval_mult : 0.85,
min_threshold_mult : 1.2,
max_threshold_mult : 1.25,
ramp_speed_mult : 1.35,
shield_capacity_mult : 0.9,
shield_drain_mult : 1.2,
shield_reward_mult : 0.85,
drone_damage_taken_mult : 1.4,
enemy_damage_taken_mult : 0.85,
mothership_damage_mult : 1.35,
mothership_repair_mult : 0.6,
drone_lives: 3,
mothership_damage_mult: 1.5,
},
"berserk" : {
id : "berserk",
label_i18n : "diff_berserk",
spawn_interval_mult : 0.7,
min_threshold_mult : 1.5,
max_threshold_mult : 1.5,
ramp_speed_mult : 1.8,
shield_capacity_mult : 0.8,
shield_drain_mult : 1.4,
shield_reward_mult : 0.7,
drone_damage_taken_mult : 1.9,
enemy_damage_taken_mult : 0.75,
mothership_damage_mult : 1.8,
mothership_repair_mult : 0.3,
drone_lives: 2,
mothership_damage_mult: 2,
},
"insane" : {
id : "insane",
label_i18n : "diff_insane",
spawn_interval_mult : 0.5,
min_threshold_mult : 1.8,
max_threshold_mult : 1.8,
ramp_speed_mult : 2.5,
shield_capacity_mult : 0.7,
shield_drain_mult : 1.7,
shield_reward_mult : 0.5,
drone_damage_taken_mult : 2.6,
enemy_damage_taken_mult : 0.6,
mothership_damage_mult : 2.5, // 2 impactos = Game Over
mothership_repair_mult : 0.0, // Sin autoreparación
drone_lives: 1,
mothership_damage_mult: 2.5,
}
},
...(typeof custom_settings == "undefined" ? {} : custom_settings)
};

View File

@ -0,0 +1,392 @@
"use strict";
/** @type {Dictionary.<string, any|null>} */
const SCSS = {
...{
"body,html" : {
height : "100%",
margin : "0em",
"background-color" : "#05070d",
color : "#e2e8f0",
"font-family" : "monospace, 'Courier New', sans-serif",
overflow : "hidden"
}
},
...{
".xgravity" : {
position : "relative",
top : "0em",
left : "0em",
width : "100%",
height : "100%",
display : "flex",
"justify-content" : "center",
"align-items" : "center"
},
".xg-view" : {
position : "absolute",
top : "0em",
left : "0em",
width : "100%",
height : "100%",
display : "flex",
"flex-direction" : "column",
"justify-content" : "center",
"align-items" : "center",
"box-sizing" : "border-box",
padding : "2em",
"text-align" : "center",
"background-color" : "rgba(5, 7, 13, 0.9)"
},
".xg-game-container" : {
position : "absolute",
top : "0em",
left : "0em",
width : "100%",
height : "100%",
overflow : "hidden",
"background-color" : "#020408"
},
".xg-canvas" : {
position : "absolute",
top : "0em",
left : "0em",
width : "100%",
height : "100%",
display : "block"
},
".xg-hud" : {
position : "absolute",
top : "0em",
left : "0em",
width : "100%",
height : "100%",
"pointer-events" : "none",
"box-sizing" : "border-box",
padding : "1em",
display : "flex",
"flex-direction" : "column",
"justify-content" : "space-between"
},
".xg-hud-top" : {
display : "flex",
"justify-content" : "space-between",
"align-items" : "flex-end",
"font-size" : "1.1em",
"font-weight" : "bold",
"text-shadow" : "0 0 0.4em #38bdf8"
},
".xg-hud-shield-container" : {
display : "flex",
"align-items" : "center",
gap : "0.5em"
},
".xg-shield-bar-bg" : {
width : "8em",
height : "0.8em",
background : "rgba(15, 23, 42, 0.8)",
border : "0.1em solid #0284c7",
"border-radius" : "0.2em",
overflow : "hidden"
},
".xg-shield-bar-fill" : {
width : "100%",
height : "100%",
background : "linear-gradient(90deg, #0284c7, #38bdf8)",
transition : "width 0.1s linear"
},
".xg-hud-bottom" : {
display : "flex",
"justify-content" : "flex-end",
"align-items" : "flex-end",
"pointer-events" : "none"
},
".xg-hud-pause-btn" : {
display : "flex",
"pointer-events" : "auto",
"font-size" : "0.85em",
padding : "0.5em 1em",
cursor : "pointer",
"z-index" : "20"
},
".xg-pause-modal" : {
position : "absolute",
top : "0em",
left : "0em",
width : "100%",
height : "100%",
background : "rgba(5, 7, 13, 0.75)",
display : "flex",
"flex-direction" : "column",
"justify-content" : "center",
"align-items" : "center",
gap : "1em",
"z-index" : "10"
},
".xg-title" : {
"font-size" : "2.5em",
"font-weight" : "bold",
"margin-bottom" : "1.2em",
color : "#38bdf8",
"text-shadow" : "0 0 0.5em #0284c7",
"letter-spacing" : "0.1em"
},
".xg-subtitle" : {
"font-size" : "1.5em",
"font-weight" : "bold",
"margin-bottom" : "0.8em",
color : "#7dd3fc"
},
".xg-menu-list" : {
display : "flex",
"flex-direction" : "column",
gap : "0.8em",
width : "14em"
},
".xg-btn" : {
background : "#0f172a",
color : "#38bdf8",
border : "0.1em solid #0284c7",
padding : "0.6em 1em",
"font-size" : "1em",
"font-family" : "inherit",
cursor : "pointer",
"border-radius" : "0.3em",
transition : "all 0.2s ease-in-out"
},
".xg-btn:hover" : {
background : "#0284c7",
color : "#ffffff",
"box-shadow" : "0 0 0.8em #38bdf8"
},
".xg-panel" : {
width : "26em",
"max-width" : "90%",
"max-height" : "28em",
"overflow-y" : "auto",
background : "rgba(15, 23, 42, 0.7)",
border : "0.1em solid #1e293b",
"border-radius" : "0.5em",
padding : "1.5em",
"box-sizing" : "border-box",
display : "flex",
"flex-direction" : "column",
gap : "1em"
},
".xg-panel::-webkit-scrollbar" : {
width : "0.4em"
},
".xg-panel::-webkit-scrollbar-track" : {
background : "rgba(15, 23, 42, 0.5)"
},
".xg-panel::-webkit-scrollbar-thumb" : {
background : "#0284c7",
"border-radius" : "0.2em"
},
".xg-panel::-webkit-scrollbar-thumb:hover" : {
background : "#38bdf8"
},
".xg-field-row" : {
display : "flex",
"justify-content" : "space-between",
"align-items" : "center",
"font-size" : "0.9em"
},
".xg-range" : {
cursor : "pointer",
width : "10em"
},
".xg-select" : {
background : "#0f172a",
color : "#38bdf8",
border : "0.1em solid #0284c7",
padding : "0.4em 0.6em",
"font-family" : "inherit",
"border-radius" : "0.3em",
cursor : "pointer"
},
".xg-credits-text" : {
"font-size" : "0.8em",
"line-height" : "1.5",
color : "#94a3b8",
"text-align" : "left"
},
".xg-link" : {
color : "#38bdf8",
"text-decoration" : "none",
"word-break" : "break-all"
},
".xg-link:hover" : {
"text-decoration" : "underline"
},
".xg-btn-selected" : {
"border-color": "#38bdf8 !important",
"box-shadow": "0 0 12px rgba(56, 189, 248, 0.4)",
color: "#e0f2fe !important"
},
".xg-btn-back" : {
"margin-top": "10px",
opacity: 0.85
},
".xg-checkbox" : {
cursor : "pointer",
width : "1.2em",
height : "1.2em",
"accent-color" : "#0284c7"
},
".xg-fps-overlay" : {
position : "absolute",
top : "0.5em",
right : "0.5em", // <-- Cambiado a la esquina superior derecha
background : "rgba(15, 23, 42, 0.75)",
border : "0.08em solid #0284c7",
"border-radius" : "0.25em",
padding : "0.2em 0.5em",
"font-size" : "0.85em",
"font-weight" : "bold",
color : "#38bdf8",
"text-shadow" : "0 0 0.3em #0284c7",
"pointer-events" : "none",
"z-index" : "9999",
"user-select" : "none"
},
},
// Capa completa superpuesta que cubre el juego y el HUD
".xg-game-over-view": {
"position": "absolute",
"inset": "0",
"display": "flex",
"flex-direction": "column",
"align-items": "center",
"justify-content": "center",
"background": "rgba(2, 6, 23, 0.78)",
"backdrop-filter": "blur(8px)",
"z-index": "100",
"padding": "20px",
"box-sizing": "border-box",
"& .xg-title": {
"font-family": "'Courier New', monospace",
"font-size": "2.8rem",
"color": "#38bdf8",
"text-shadow": "0 0 20px rgba(56, 189, 248, 0.6)",
"letter-spacing": "4px",
"margin": "0 0 10px 0"
},
// Insignia de motivo de derrota
"& .xg-game-over-reason": {
"margin-bottom": "22px",
"& .xg-badge-danger": {
"display": "inline-block",
"padding": "6px 14px",
"background": "rgba(239, 68, 68, 0.15)",
"border": "1px solid rgba(239, 68, 68, 0.5)",
"color": "#f87171",
"font-family": "monospace",
"font-size": "0.9rem",
"letter-spacing": "2px",
"border-radius": "4px",
"text-transform": "uppercase"
}
},
// Tarjeta central de estadísticas
"& .xg-stats-card": {
"width": "100%",
"max-width": "460px",
"background": "rgba(15, 23, 42, 0.85)",
"border": "1px solid rgba(56, 189, 248, 0.35)",
"box-shadow": "0 0 25px rgba(2, 6, 23, 0.9), inset 0 0 15px rgba(56, 189, 248, 0.08)",
"border-radius": "6px",
"padding": "16px 20px",
"margin-bottom": "24px",
"display": "flex",
"flex-direction": "column",
"gap": "10px",
"box-sizing": "border-box"
},
// Filas de estadísticas: aquí se resuelve la separación del texto
"& .xg-stat-row": {
"display": "flex",
"justify-content": "space-between",
"align-items": "center",
"width": "100%",
"padding": "6px 0",
"border-bottom": "1px dashed rgba(56, 189, 248, 0.15)",
"font-family": "monospace",
"font-size": "1rem",
"&:last-child": {
"border-bottom": "none"
},
"& .xg-stat-label": {
"color": "#94a3b8",
"letter-spacing": "1px",
"text-align": "left"
},
"& .xg-stat-value": {
"color": "#f1f5f9",
"font-weight": "bold",
"text-align": "right",
"letter-spacing": "1px"
},
// Fila destacada de la puntuación
"&.xg-stat-score": {
"border-bottom": "1px solid rgba(56, 189, 248, 0.4)",
"padding-bottom": "10px",
"margin-bottom": "4px",
"& .xg-val-score": {
"color": "#38bdf8",
"font-size": "1.25rem",
"text-shadow": "0 0 10px rgba(56, 189, 248, 0.5)"
}
}
},
// Botonera inferior
"& .xg-menu-list": {
"display": "flex",
"flex-direction": "column",
"gap": "12px",
"width": "100%",
"max-width": "280px",
"& .xg-btn": {
"width": "100%",
"padding": "12px 0",
"font-family": "monospace",
"font-size": "1.05rem",
"letter-spacing": "2px"
}
}
},
".xg-main-menu": {
// ... tus estilos actuales del menú ...
"& .xg-menu-header": {
"display": "flex",
"flex-direction": "column",
"align-items": "center",
"justify-content": "center",
"margin-bottom": "24px"
},
"& .xg-main-logo": {
"width": "120px",
"height": "120px",
"margin-bottom": "14px",
"filter": "drop-shadow(0 0 16px rgba(56, 189, 248, 0.45))",
"transition": "transform 0.3s ease, filter 0.3s ease",
"user-select": "none",
"pointer-events": "none"
}
},
...(typeof custom_styles == "undefined" ? {} : custom_styles)
};

View File

@ -0,0 +1,80 @@
"use strict";
const CreditsView = (function(){
const CreditsView = function(xgravity){
const self = this;
let view_element = null;
this.close = () => {
if(view_element && view_element.parentNode){
view_element.parentNode.removeChild(view_element);
view_element = null;
}
};
this.go = () => {
self.close();
const repo_url = "https://git.k3y.pw/KyMAN/XGravity";
const web_url = "https://xgravity.k3y.pw/";
const photographer_url = "https://unsplash.com/@vincentiu";
const opengameart_url = "https://opengameart.org/content/space-game-art-pack-extended";
const nodes = xgravity.html(xgravity.item_self, [
"div", { class : "xg-view xg-credits" }, [
["h2", { class : "xg-subtitle", "data-i18n" : "cred_title" }],
["div", { class : "xg-panel xg-credits-text" }, [
["p", {}, [
["strong", { "data-i18n" : "cred_original_concept" }],
["br"],
["span", { "data-i18n" : "cred_author" }]
]],
["p", { "data-i18n" : "cred_original_tech" }],
["p", { "data-i18n" : "cred_old_urls" }],
["p", {}, [
["strong", { "data-i18n" : "cred_remake" }],
["br"],
["span", { "data-i18n" : "cred_remake_authors" }]
]],
["p", {}, [
["strong", { "data-i18n" : "cred_assets" }],
["br"],
["span", { "data-i18n" : "cred_bg_photo" }],
["br"],
["a", { class : "xg-link", href : photographer_url, target : "_blank" }, photographer_url],
["br"],
["br"],
["span", { "data-i18n" : "cred_tatermand_art" }],
["br"],
["a", { class : "xg-link", href : opengameart_url, target : "_blank" }, opengameart_url]
]],
["p", {}, [
["strong", { "data-i18n" : "cred_links" }],
["br"],
["a", { class : "xg-link", href : web_url, target : "_blank" }, web_url],
["br"],
["a", { class : "xg-link", href : repo_url, target : "_blank" }, repo_url]
]],
["button", {
class : "xg-btn",
"data-i18n" : "btn_back",
onclick : () => {
self.close();
if(xgravity.views.main_menu)
xgravity.views.main_menu.go();
}
}]
]]
]
]);
view_element = nodes[0];
xgravity.i18n.update();
};
};
return CreditsView;
})();

View File

@ -0,0 +1,89 @@
"use strict";
const DifficultySelectView = (function(){
const DifficultySelectView = function(xgravity){
const self = this;
let view_element = null;
this.close = () => {
if(view_element && view_element.parentNode){
view_element.parentNode.removeChild(view_element);
view_element = null;
}
};
this.go = () => {
self.close();
const is_game_active = xgravity.views.game && typeof xgravity.views.game.is_active === "function"
? xgravity.views.game.is_active()
: false;
const presets = xgravity.settings.get("difficulty_presets", null, {});
const current_diff = xgravity.settings.get("current_difficulty", null, "average");
// Lista ordenada de niveles
const diff_keys = ["training", "newbie", "lazy", "average", "hard", "berserk", "insane"];
const diff_buttons = [];
for(let i = 0; i < diff_keys.length; i++){
const key = diff_keys[i];
const preset = presets[key];
if(!preset) continue;
const is_selected = (key === current_diff);
diff_buttons.push([
"button", {
class : `xg-btn xg-btn-diff ${is_selected ? "xg-btn-selected" : ""}`,
"data-i18n" : preset.label_i18n || `diff_${key}`,
onclick : () => {
// 1. Guardar la dificultad seleccionada
xgravity.settings.add({ current_difficulty : key });
// 2. Desmontar esta vista
self.close();
// 3. Iniciar la partida limpia
if(xgravity.views.game){
if(is_game_active){
xgravity.views.game.stop();
}
xgravity.views.game.go();
}
}
}
]);
}
// Botón Volver (cierra esta vista y regresa al Menú Principal)
diff_buttons.push([
"button", {
class : "xg-btn xg-btn-back",
"data-i18n" : "btn_back",
onclick : () => {
self.close();
if(xgravity.views.main_menu){
xgravity.views.main_menu.go();
}
}
}
]);
const nodes = xgravity.html(xgravity.item_self, [
"div", { class : "xg-view xg-difficulty-select" }, [
["h2", { class : "xg-subtitle", "data-i18n" : "opt_difficulty" }],
["div", { class : "xg-menu-list" }, diff_buttons]
]
]);
view_element = nodes[0];
xgravity.i18n.update();
};
};
return DifficultySelectView;
})();

View File

@ -0,0 +1,78 @@
"use strict";
const FpsOverlayView = (function(){
const FpsOverlayView = function(xgravity){
const self = this;
let view_element = null;
let label_element = null;
let frame_count = 0;
let accumulated_time = 0;
let last_fps = 60;
this.close = () => {
if(view_element && view_element.parentNode){
view_element.parentNode.removeChild(view_element);
view_element = null;
label_element = null;
}
};
this.tick = (delta_time) => {
const enabled = xgravity.settings.get("show_fps", null, false);
const is_game_active = xgravity.views.game
&& typeof xgravity.views.game.is_active === "function"
&& xgravity.views.game.is_active()
&& !xgravity.views.game.is_paused();
// Solo se muestra si está activado en ajustes Y hay partida activa no pausada
if(!enabled || !is_game_active){
if(view_element) self.close();
return;
}
if(!view_element){
self.go();
}
frame_count++;
accumulated_time += delta_time;
if(accumulated_time >= 0.25){
last_fps = Math.round(frame_count / accumulated_time);
frame_count = 0;
accumulated_time = 0;
if(label_element){
label_element.textContent = `${last_fps} FPS`;
if(last_fps >= 50){
label_element.style.color = "#38bdf8";
} else if(last_fps >= 30){
label_element.style.color = "#facc15";
} else {
label_element.style.color = "#f43f5e";
}
}
}
};
this.go = () => {
if(view_element && view_element.parentNode) return;
const nodes = xgravity.html(xgravity.item_self, [
"div", { class : "xg-fps-overlay" }, [
["span", { id : "xg_fps_counter" }, "60 FPS"]
]
]);
view_element = nodes[0];
label_element = view_element.querySelector("#xg_fps_counter");
};
};
return FpsOverlayView;
})();

View File

@ -0,0 +1,107 @@
"use strict";
const GameOverView = (function(){
const GameOverView = function(xgravity){
const self = this;
let view_element = null;
this.close = () => {
if(view_element && view_element.parentNode){
view_element.parentNode.removeChild(view_element);
view_element = null;
}
};
this.go = (stats = {}) => {
self.close();
const score = stats.score || 0;
const score_str = score.toString().padStart(8, "0");
const reason = stats.reason || "mothership_destroyed";
const diff_id = stats.difficulty || "average";
const spawned = stats.total_spawned || 0;
const destroyed = stats.total_destroyed || 0;
const shield_kills = stats.shield_kills || 0;
const clear_rate = spawned > 0 ? ((destroyed / spawned) * 100).toFixed(1) : "0.0";
const reason_text = reason === "all_drones_lost"
? "DEFEAT: ALL DRONES LOST IN ACTION"
: "CRITICAL: MOTHERSHIP HULL BREACHED";
const action_buttons = [
[
"button", {
class : "xg-btn xg-btn-resume",
"data-i18n" : "btn_retry",
onclick : () => {
self.close();
if(xgravity.views.game){
xgravity.views.game.go();
}
}
}
],
[
"button", {
class : "xg-btn",
"data-i18n" : "btn_main_menu",
onclick : () => {
self.close();
if(xgravity.views.game){
xgravity.views.game.stop();
}
if(xgravity.views.main_menu){
xgravity.views.main_menu.go();
}
}
}
]
];
const nodes = xgravity.html(xgravity.item_self, [
"div", { class : "xg-view xg-game-over-view" }, [
["h1", { class : "xg-title", "data-i18n" : "game_over_title" }],
["div", { class : "xg-game-over-reason" }, [
["span", { class : "xg-badge-danger" }, reason_text]
]],
["div", { class : "xg-menu-list xg-stats-card" }, [
["div", { class : "xg-stat-row xg-stat-score" }, [
["span", { class : "xg-stat-label", "data-i18n" : "stat_final_score" }],
["span", { class : "xg-stat-value xg-val-score" }, `[ ${score_str} ]`]
]],
["div", { class : "xg-stat-row" }, [
["span", { class : "xg-stat-label", "data-i18n" : "stat_difficulty" }],
["span", { class : "xg-stat-value" }, diff_id.toUpperCase()]
]],
["div", { class : "xg-stat-row" }, [
["span", { class : "xg-stat-label", "data-i18n" : "stat_enemies_spawned" }],
["span", { class : "xg-stat-value" }, `${spawned}`]
]],
["div", { class : "xg-stat-row" }, [
["span", { class : "xg-stat-label", "data-i18n" : "stat_enemies_destroyed" }],
["span", { class : "xg-stat-value" }, `${destroyed}`]
]],
["div", { class : "xg-stat-row" }, [
["span", { class : "xg-stat-label", "data-i18n" : "stat_shield_kills" }],
["span", { class : "xg-stat-value" }, `${shield_kills}`]
]],
["div", { class : "xg-stat-row" }, [
["span", { class : "xg-stat-label", "data-i18n" : "stat_clear_rate" }],
["span", { class : "xg-stat-value" }, `${clear_rate}%`]
]]
]],
["div", { class : "xg-menu-list" }, action_buttons]
]
]);
view_element = nodes[0];
xgravity.i18n.update();
};
};
return GameOverView;
})();

View File

@ -0,0 +1,683 @@
"use strict";
const GameView = (function(){
const GameView = function(xgravity){
const self = this;
let view_element = null,
canvas_element = null,
ctx = null,
animation_frame_id = null,
last_timestamp = 0,
is_paused = false,
pause_modal = null,
bg_manager = null,
current_bg_mode = null;
let cockpit_model = null;
let drone_model = null;
let hud_model = null;
let enemies_manager = null;
// let explosions_model = null;
// Variables de balanceo e inercia de cámara
let camera_drift_x = 0;
let camera_drift_y = 0;
let camera_sway_clock = 0;
let bg_parallax_x = 0;
let bg_parallax_y = 0;
// Variables de Screen Shake
let shake_trauma = 0.0;
let shake_duration = 0.0;
let shake_max_duration = 0.1;
let shake_intensity = 0.0;
let shake_offset_x = 0;
let shake_offset_y = 0;
let shake_angle = 0;
// Estado del juego
let current_score = 0;
let screen_shake_intensity = 0;
let screen_shake_decay = 4.0;
let drone_lives = 3;
let max_drone_lives = 3;
let is_respawning_drone = false;
let is_game_over = false;
let session_stats = {
total_spawned : 0,
total_destroyed : 0,
shield_kills : 0
};
// const trigger_mega_shake = () => {
// screen_shake_intensity = 28.0; // Temblores violentos en píxeles
// };
// Métodos de control de estado expuestos
this.is_active = () => animation_frame_id !== null;
this.is_paused = () => is_paused;
this.pause = () => {
is_paused = true;
if(xgravity.views.fps){
xgravity.views.fps.close();
}
};
this.resume = () => {
is_paused = false;
last_timestamp = 0; // Evita saltos de delta_time al volver
};
const on_pointer_move = (e) => {
if(!canvas_element || !drone_model) return;
const rect = canvas_element.getBoundingClientRect();
const client_x = e.touches ? e.touches[0].clientX : e.clientX;
const client_y = e.touches ? e.touches[0].clientY : e.clientY;
const scale_x = canvas_element.width / rect.width;
const scale_y = canvas_element.height / rect.height;
const canvas_pixel_x = (client_x - rect.left) * scale_x;
const canvas_pixel_y = (client_y - rect.top) * scale_y;
drone_model.set_target_pixels(canvas_pixel_x, canvas_pixel_y, canvas_element.width, canvas_element.height);
};
// Controladores de activación de escudo
const on_mouse_down = (e) => {
if(e.button === 0 && drone_model){
drone_model.set_shield_active(true);
}
};
const on_mouse_up = (e) => {
if(e.button === 0 && drone_model){
drone_model.set_shield_active(false);
}
};
const on_key_down = (e) => {
// if((e.code === "Space" || e.key === " ") && !e.repeat && drone_model){
// drone_model.set_shield_active(true);
// }
// Tecla ESC: abre/cierra menú o pausa la partida
if(e.code === "Escape" || e.key === "Escape"){
e.preventDefault();
self.toggle_pause();
return;
}
const action_key = xgravity.settings.get("action_key", null, "Space");
const matches_action = (action_key === "Space" && (e.code === "Space" || e.key === " ")) ||
(e.code === action_key || e.key.toUpperCase() === action_key.toUpperCase());
if(matches_action && !e.repeat && drone_model){
drone_model.set_shield_active(true);
}
};
const on_key_up = (e) => {
// if((e.code === "Space" || e.key === " ") && drone_model){
// drone_model.set_shield_active(false);
// }
const action_key = xgravity.settings.get("action_key", null, "Space");
const matches_action = (action_key === "Space" && (e.code === "Space" || e.key === " ")) ||
(e.code === action_key || e.key.toUpperCase() === action_key.toUpperCase());
if(matches_action && drone_model){
drone_model.set_shield_active(false);
}
};
const on_touch_start = (e) => {
if(drone_model){
drone_model.set_shield_active(true);
}
};
const on_touch_end = (e) => {
if(drone_model){
drone_model.set_shield_active(false);
}
};
// Callback cuando el escudo destruye a un enemigo
const on_enemy_destroyed = (enemy, impact_damage = 0) => {
const bounds = enemy.get_bounds();
// 1. Efecto visual de destrucción
if(xgravity.explosions){
xgravity.explosions.add(bounds.x, bounds.y, "shield");
}
trigger_screen_shake("shield");
// 2. Puntos base configurados en el enemigo
const base_pts = (typeof enemy.get_points === "function") ? enemy.get_points() : 100;
// 3. MULTIPLICADOR CINÉTICO:
// Si el daño fue 20 -> x1.20 | Si embistes con inercia y fue 65 -> x1.65 | Si fue 120 -> x2.20
const kinetic_mult = impact_damage > 0 ? (1.0 + (impact_damage * 0.01)) : 1.0;
const final_pts = Math.round(base_pts * kinetic_mult);
// 4. Sumar puntos y actualizar el HUD inmediatamente
current_score += final_pts;
if(hud_model){
hud_model.set_score(current_score);
}
// 5. Estadísticas de la partida
session_stats.total_destroyed++;
session_stats.shield_kills++;
// 6. Recarga del escudo según dificultad
if(drone_model){
const base_reward = xgravity.settings.get("base_drone_shield_kill_reward", null, 10.0);
const preset = xgravity.settings.get_difficulty_preset();
const reward = base_reward * (preset ? preset.shield_reward_mult : 1.0);
drone_model.restore_shield(reward);
}
};
const on_mothership_impact = (base_damage = 25) => {
console.log("¡BOOM! Impacto en cabina recibido con daño base:", base_damage);
const preset = xgravity.settings.get_difficulty_preset();
const dmg_mult = preset ? preset.mothership_damage_mult : 1.0;
const final_damage = base_damage * dmg_mult;
if(xgravity.audio){
xgravity.audio.play("mothership_impact");
}
if(cockpit_model && typeof cockpit_model.take_damage === "function"){
const is_destroyed = cockpit_model.take_damage(final_damage);
if(is_destroyed){
on_game_over("mothership_destroyed");
}
}
// Disparo del temblor brutal de la nodriza
trigger_screen_shake("mothership");
if(xgravity.explosions && typeof xgravity.explosions.add_mothership_impact === "function"){
xgravity.explosions.add_mothership_impact();
}
};
// Callback cuando impacta al dron sin escudo
const on_drone_hit = (enemy, damage_to_drone = 35) => {
const bounds = enemy.get_bounds();
if(xgravity.explosions){
xgravity.explosions.add(bounds.x, bounds.y, "hit");
}
trigger_screen_shake("hit");
if(cockpit_model) cockpit_model.set_alert(true);
if(drone_model && damage_to_drone > 0){
drone_model.damage_shield(damage_to_drone);
// Si el escudo/vida cae a 0, estalla y entra el siguiente
if(drone_model.get_position().shield <= 0){
destroy_current_drone();
}
}
};
// Callback cuando el enemigo sobrevive al choque (chispas y deflexión)
const on_enemy_deflected = (x, y, nx, ny, intensity, damage, shield_active) => {
if(xgravity.explosions){
xgravity.explosions.add_sparks(x, y, nx, ny, intensity);
}
if(!shield_active && drone_model){
drone_model.damage_shield(damage * 0.5);
trigger_screen_shake("shield");
} else if(shield_active && drone_model){
drone_model.damage_shield(damage * 0.2);
}
};
const on_game_over = (reason = "mothership_destroyed") => {
if(is_game_over) return;
is_game_over = true;
console.log("=== GAME OVER ===", reason);
// Ocultamos el botón de pausa para que no estorbe en el Game Over
if(view_element){
const pause_btn = view_element.querySelector(".xg-hud-pause-btn");
if(pause_btn) pause_btn.style.display = "none";
}
trigger_mega_shake();
// Si EnemiesManager lleva la cuenta de spawns, sincronizamos:
if(enemies_manager && typeof enemies_manager.get_total_spawned === "function"){
session_stats.total_spawned = enemies_manager.get_total_spawned();
}
const preset = (xgravity.settings && typeof xgravity.settings.get_difficulty_preset === "function")
? xgravity.settings.get_difficulty_preset()
: null;
const final_data = {
score : current_score,
reason : reason,
difficulty : preset ? preset.id : "average",
total_spawned : Math.max(session_stats.total_spawned, session_stats.total_destroyed),
total_destroyed : session_stats.total_destroyed,
shield_kills : session_stats.shield_kills
};
// Pausar el juego y abrir la pantalla tras un breve retardo dramático
setTimeout(() => {
self.pause();
if(xgravity.views.game_over){
xgravity.views.game_over.go(final_data);
}
}, 600);
};
const destroy_current_drone = () => {
if(!drone_model || drone_model.is_deploying() || is_respawning_drone || is_game_over) return;
is_respawning_drone = true;
const pos = drone_model.get_position();
// 1. Explosión de destrucción en el plano de batalla espacial
if(xgravity.explosions){
for(let k = 0; k < 3; k++){
xgravity.explosions.add(pos.cell_x + ((Math.random() - 0.5) * 1.5), pos.cell_y + ((Math.random() - 0.5) * 1.5), "hit");
}
xgravity.explosions.add_sparks(pos.cell_x, pos.cell_y, 0, -1, 3.0);
}
trigger_screen_shake("mothership");
// 2. Restar vida
drone_lives = Math.max(0, drone_lives - 1);
if(hud_model){
hud_model.set_lives(drone_lives, max_drone_lives);
}
// 3. Evaluar derrota o siguiente dron
if(drone_lives <= 0){
on_game_over("all_drones_lost");
} else {
// Pequeña pausa dramática para que se aprecie la explosión (~0.4s) antes de que la nodriza lance el siguiente
setTimeout(() => {
if(drone_model && !is_game_over){
drone_model.restore_shield(drone_model.get_position().max_shield || 100);
drone_model.start_deployment(); // Emerge a tamaño x10 desde abajo
}
is_respawning_drone = false;
}, 400);
}
};
// Disparador de sacudida con perfiles de intensidad
const trigger_screen_shake = (type = "shield") => {
shake_trauma = 1.0;
if(type === "shield"){
// Golpe amortiguado en el escudo del dron
shake_intensity = xgravity.settings.get("screen_shake_shield_intensity", null, 0.25);
shake_max_duration = xgravity.settings.get("screen_shake_shield_duration", null, 0.15);
} else if(type === "mothership" || type === "critical"){
// IMPACTO CRÍTICO EN CABINA: Sacudida masiva y violenta
shake_intensity = xgravity.settings.get("screen_shake_mothership_intensity", null, 4.2); // ~4.2 celdas de amplitud
shake_max_duration = xgravity.settings.get("screen_shake_mothership_duration", null, 1.15); // >1 segundo de temblor
} else {
// Golpe en el dron sin escudo
shake_intensity = xgravity.settings.get("screen_shake_hit_intensity", null, 0.85);
shake_max_duration = xgravity.settings.get("screen_shake_hit_duration", null, 0.42);
}
shake_duration = shake_max_duration;
};
const trigger_mega_shake = () => {
trigger_screen_shake("mothership");
};
const sync_background = () => {
const mode = xgravity.settings.get("background_mode", null, "stars");
if(current_bg_mode !== mode || !bg_manager){
current_bg_mode = mode;
if(mode === "image")
bg_manager = new ImageBackground(xgravity);
else if(mode === "procedural")
bg_manager = new ProceduralBackground(xgravity);
else
bg_manager = new StarsBackground(xgravity);
if(canvas_element)
bg_manager.resize(canvas_element.width, canvas_element.height);
}
};
const resize_canvas = () => {
if(!canvas_element) return;
const quality_pct = xgravity.settings.get("canvas_quality", null, 100) / 100;
const rect = canvas_element.getBoundingClientRect();
const internal_w = Math.max(1, Math.floor(rect.width * quality_pct));
const internal_h = Math.max(1, Math.floor(rect.height * quality_pct));
if(canvas_element.width !== internal_w || canvas_element.height !== internal_h){
canvas_element.width = internal_w;
canvas_element.height = internal_h;
if(bg_manager)
bg_manager.resize(internal_w, internal_h);
}
};
const update_camera = (delta_time, cell_px) => {
camera_sway_clock += delta_time;
const sway_speed = xgravity.settings.get("cockpit_sway_speed", null, 0.8);
if(drone_model){
const drone_pos = drone_model.get_position();
// Inercia de cabina
const drift_factor = xgravity.settings.get("cockpit_drift_amount", null, 0.6);
const target_drift_x = (-drone_pos.cell_x / 20.0) * drift_factor;
const target_drift_y = (-drone_pos.cell_y / 20.0) * drift_factor;
camera_drift_x += (target_drift_x - camera_drift_x) * 4.0 * delta_time;
camera_drift_y += (target_drift_y - camera_drift_y) * 4.0 * delta_time;
// Parallax del fondo estelar
const bg_factor = xgravity.settings.get("background_parallax_amount", null, 0.25);
const target_bg_x = (drone_pos.cell_x / 20.0) * bg_factor * cell_px;
const target_bg_y = (drone_pos.cell_y / 20.0) * bg_factor * cell_px;
bg_parallax_x += (target_bg_x - bg_parallax_x) * 3.0 * delta_time;
bg_parallax_y += (target_bg_y - bg_parallax_y) * 3.0 * delta_time;
}
// Screen Shake
if(shake_duration > 0){
shake_duration -= delta_time;
shake_trauma = shake_max_duration > 0 ? Math.max(0, shake_duration / shake_max_duration) : 0;
// Decaimiento no lineal (trauma al cuadrado): arranca fuertísimo y decae suave
const trauma_factor = shake_trauma * shake_trauma;
const shake_offset_cells = trauma_factor * shake_intensity;
shake_offset_x = (Math.random() * 2 - 1) * shake_offset_cells * cell_px;
shake_offset_y = (Math.random() * 2 - 1) * shake_offset_cells * cell_px;
// Si es un impacto de nodriza (shake_intensity > 2.0), el latigazo angular se triplica (~0.08 rad ≈ 4.6°)
const max_roll = (shake_intensity > 2.0) ? 0.08 : 0.025;
shake_angle = (Math.random() * 2 - 1) * (trauma_factor * max_roll);
} else {
shake_offset_x = 0;
shake_offset_y = 0;
shake_angle = 0;
}
};
const game_loop = (timestamp) => {
if(!last_timestamp) last_timestamp = timestamp;
const delta_time = Math.min((timestamp - last_timestamp) / 1000, 0.1);
last_timestamp = timestamp;
const cell_size = (canvas_element.width < canvas_element.height ? canvas_element.width : canvas_element.height) / xgravity.settings.get("cells", null, 40);
if(!is_paused){
sync_background();
resize_canvas();
if(xgravity.views.fps){
xgravity.views.fps.tick(delta_time);
}
// 1. FÍSICAS Y ACTUALIZACIONES (Aquí solo van updates)
if(enemies_manager){
enemies_manager.update(delta_time, drone_model, on_enemy_destroyed, on_drone_hit, on_enemy_deflected, on_mothership_impact);
}
if(drone_model){
drone_model.update(delta_time);
}
if(cockpit_model){
cockpit_model.update(delta_time);
}
if(xgravity.explosions){
xgravity.explosions.update(delta_time); // <-- ACTIVADO
}
if(hud_model){
hud_model.render(ctx, canvas_element.width, canvas_element.height, delta_time, drone_model, cockpit_model, current_score, drone_lives, max_drone_lives);
}
// 2. CÁMARA Y TRAUMA
update_camera(delta_time, cell_size);
// 3. LIMPIEZA
ctx.clearRect(0, 0, canvas_element.width, canvas_element.height);
// 4. FONDO ESTELAR CON PARALLAX
if(bg_manager){
ctx.save();
ctx.translate(bg_parallax_x + (shake_offset_x * 0.4), bg_parallax_y + (shake_offset_y * 0.4));
bg_manager.render(ctx, canvas_element.width, canvas_element.height, delta_time);
ctx.restore();
}
// 5. SCREEN SHAKE ACTIVO PARA ENTIDADES Y CABINA
ctx.save();
const cx = canvas_element.width / 2;
const cy = canvas_element.height / 2;
ctx.translate(cx + shake_offset_x, cy + shake_offset_y);
if(shake_angle !== 0) ctx.rotate(shake_angle);
ctx.translate(-cx, -cy);
// A) Enemigos en el plano de batalla (debajo del dron)
if(enemies_manager){
enemies_manager.render_under(ctx, canvas_element.width, canvas_element.height);
}
// B) Dron y explosiones de combate espacial
if(drone_model){
drone_model.render(ctx, canvas_element.width, canvas_element.height);
}
if(xgravity.explosions){
xgravity.explosions.render(ctx, canvas_element.width, canvas_element.height);
}
// C) Enemigos en sobrevuelo + Retículas y Daño flotante
if(enemies_manager){
enemies_manager.render_over(ctx, canvas_element.width, canvas_element.height);
}
// D) Cúpula / Cabina con balanceo inercial
ctx.save();
const drift_px_x = (camera_drift_x + Math.sin(camera_sway_clock * 0.8) * 0.15) * cell_size;
const drift_px_y = (camera_drift_y + Math.cos(camera_sway_clock * 0.56) * 0.15) * cell_size;
ctx.translate(drift_px_x, drift_px_y);
if(cockpit_model){
cockpit_model.render(ctx, canvas_element.width, canvas_element.height, delta_time);
}
ctx.restore();
// E) EFECTOS DE CABINA (Flash de pantalla y fragmentos de roca sobre el cristal)
if(xgravity.explosions && typeof xgravity.explosions.render_cockpit === "function"){
xgravity.explosions.render_cockpit(ctx, canvas_element.width, canvas_element.height);
}
ctx.restore(); // Cierre de Screen Shake
// 6. HUD HOLOGRÁFICO (fijo en cabina)
if(hud_model){
hud_model.render(ctx, canvas_element.width, canvas_element.height, delta_time, drone_model, cockpit_model, current_score);
}
}
animation_frame_id = requestAnimationFrame(game_loop);
};
this.update_hud = () => {};
this.toggle_pause = () => {
if(!is_paused){
self.pause();
if(xgravity.views.main_menu){
xgravity.views.main_menu.go();
}
} else {
// Si ya estaba pausado y pulsa ESC de nuevo, reanudamos
if(xgravity.views.main_menu){
xgravity.views.main_menu.close();
}
self.resume();
}
};
this.stop = () => {
if(animation_frame_id){
cancelAnimationFrame(animation_frame_id);
animation_frame_id = null;
}
if(view_element && view_element.parentNode){
view_element.parentNode.removeChild(view_element);
view_element = null;
}
if(xgravity.views.fps){
xgravity.views.fps.close();
}
window.removeEventListener("mousedown", on_mouse_down);
window.removeEventListener("mouseup", on_mouse_up);
window.removeEventListener("keydown", on_key_down);
window.removeEventListener("keyup", on_key_up);
window.removeEventListener("touchstart", on_touch_start);
window.removeEventListener("touchend", on_touch_end);
window.removeEventListener("mousemove", on_pointer_move);
window.removeEventListener("touchmove", on_pointer_move);
cockpit_model = null;
drone_model = null;
hud_model = null;
enemies_manager = null;
// explosions_model = null;
bg_manager = null;
current_bg_mode = null;
};
this.go = () => {
self.stop();
is_paused = false;
current_score = 0;
last_timestamp = 0;
cockpit_model = new CockpitModel(xgravity);
drone_model = new DroneModel(xgravity);
hud_model = new TelemetryHudModel(xgravity);
enemies_manager = new EnemiesManager(xgravity);
// explosions_model = new ExplosionsModel(xgravity);
window.addEventListener("mousedown", on_mouse_down);
window.addEventListener("mouseup", on_mouse_up);
window.addEventListener("keydown", on_key_down);
window.addEventListener("keyup", on_key_up);
window.addEventListener("touchstart", on_touch_start, { passive : true });
window.addEventListener("touchend", on_touch_end, { passive : true });
window.addEventListener("mousemove", on_pointer_move);
window.addEventListener("touchmove", on_pointer_move, { passive : true });
session_stats = {
total_spawned : 0,
total_destroyed : 0,
shield_kills : 0
};
if(enemies_manager){
enemies_manager.set_on_mothership_impact((base_damage, impact_x, impact_y) => {
// 1. Calcular daño mitigado o potenciado por la dificultad
const preset = xgravity.settings.get_difficulty_preset();
const dmg_mult = preset ? preset.mothership_damage_mult : 1.0;
const final_damage = base_damage * dmg_mult;
// 2. Aplicar daño a la nave nodriza
let is_destroyed = false;
if(cockpit_model && typeof cockpit_model.take_damage === "function"){
is_destroyed = cockpit_model.take_damage(final_damage);
}
// 3. Efectos visuales brutales
trigger_mega_shake();
const cx = canvas.width / 2;
const cy = canvas.height / 2;
if(xgravity.explosions && typeof xgravity.explosions.add_mothership_impact === "function"){
xgravity.explosions.add_mothership_impact(cx, cy);
}
// 4. Si el casco llega a cero: Game Over
if(is_destroyed){
// Trigger de fin de partida
console.log("¡NAVE NODRIZA DESTRUIDA!");
}
});
}
const nodes = xgravity.html(xgravity.item_self, [
"div", { class : "xg-game-container" }, [
["canvas", { class : "xg-canvas" }],
["div", { class : "xg-hud" }, [
// Arriba (FPS y telemetría)
["div", { class : "xg-hud-top" }],
// Abajo a la derecha: Botón de Pausa
["div", { class : "xg-hud-bottom" }, [
["button", {
class : "xg-btn xg-hud-pause-btn",
"data-i18n" : "hud_pause_title",
onclick : () => self.toggle_pause()
}]
]]
]]
]
]);
const preset = (xgravity.settings && typeof xgravity.settings.get_difficulty_preset === "function")
? xgravity.settings.get_difficulty_preset()
: null;
max_drone_lives = (preset && preset.drone_lives !== undefined)
? preset.drone_lives
: xgravity.settings.get("drone_lives", null, 3);
drone_lives = max_drone_lives;
is_game_over = false;
is_respawning_drone = false;
if(hud_model){
hud_model.set_lives(drone_lives, max_drone_lives);
}
view_element = nodes[0];
canvas_element = view_element.querySelector("canvas");
ctx = canvas_element.getContext("2d");
sync_background();
resize_canvas();
self.update_hud();
xgravity.i18n.update();
animation_frame_id = requestAnimationFrame(game_loop);
};
};
return GameView;
})();

View File

@ -0,0 +1,101 @@
"use strict";
const MainMenuView = (function(){
const MainMenuView = function(xgravity){
const self = this;
let view_element = null;
this.close = () => {
if(view_element && view_element.parentNode){
view_element.parentNode.removeChild(view_element);
view_element = null;
}
};
this.go = () => {
self.close();
const is_game_active = xgravity.views.game && typeof xgravity.views.game.is_active === "function"
? xgravity.views.game.is_active()
: false;
const menu_buttons = [];
// 1. Si hay partida en curso: botón Reanudar
if(is_game_active){
menu_buttons.push([
"button", {
class : "xg-btn xg-btn-resume",
"data-i18n" : "btn_resume",
onclick : () => {
self.close();
if(xgravity.views.game){
xgravity.views.game.resume();
}
}
}
]);
}
// 2. Jugar / Nueva Partida -> Abre DifficultySelectView
menu_buttons.push([
"button", {
class : "xg-btn",
"data-i18n" : is_game_active ? "btn_new_game" : "btn_play",
onclick : () => {
self.close();
if(xgravity.views.difficulty_select){
xgravity.views.difficulty_select.go();
}
}
}
]);
// 3. Vistas secundarias
menu_buttons.push(
["button", {
class : "xg-btn",
"data-i18n" : "btn_options",
onclick : () => {
self.close();
if(xgravity.views.options) xgravity.views.options.go();
}
}],
["button", {
class : "xg-btn",
"data-i18n" : "btn_ranking",
onclick : () => {
self.close();
if(xgravity.views.ranking) xgravity.views.ranking.go();
}
}],
["button", {
class : "xg-btn",
"data-i18n" : "btn_credits",
onclick : () => {
self.close();
if(xgravity.views.credits) xgravity.views.credits.go();
}
}]
);
const nodes = xgravity.html(xgravity.item_self, [
"div", { class : "xg-view xg-main-menu" }, [
["div", { class : "xg-menu-header" }, [
["img", { class : "xg-main-logo", src : "images/logo.svg", alt : "XGravity Logo" }],
["h1", { class : "xg-title", "data-i18n" : "xgravity" }]
]],
["div", { class : "xg-menu-list" }, menu_buttons]
]
]);
view_element = nodes[0];
xgravity.i18n.update();
};
};
return MainMenuView;
})();

View File

@ -0,0 +1,150 @@
"use strict";
const OptionsView = (function(){
const OptionsView = function(xgravity){
const self = this;
let view_element = null,
listening_key = false;
this.close = () => {
if(view_element && view_element.parentNode){
view_element.parentNode.removeChild(view_element);
view_element = null;
}
};
this.go = () => {
self.close();
let current_quality = xgravity.settings.get("canvas_quality", null, 100);
let current_action_key = xgravity.settings.get("action_key", null, "Space");
let current_bg_mode = xgravity.settings.get("background_mode", null, "stars");
let current_diff = xgravity.settings.get("current_difficulty", null, "average");
const nodes = xgravity.html(xgravity.item_self, [
"div", { class : "xg-view xg-options" }, [
["h2", { class : "xg-subtitle", "data-i18n" : "opt_title" }],
["div", { class : "xg-panel" }, [
// Mostrar / Ocultar FPS
["div", { class : "xg-field-row" }, [
["span", { "data-i18n" : "opt_show_fps" }],
["input", {
type : "checkbox",
class : "xg-checkbox",
...(xgravity.settings.get("show_fps", null, false) ? { checked : "checked" } : {}),
"data-i18n-without" : "true",
onchange : (input) => {
const enabled = input.checked;
xgravity.settings.add({ show_fps : enabled });
// Si se desactiva, lo cerramos inmediatamente
if(!enabled && xgravity.views.fps){
xgravity.views.fps.close();
}
}
}]
]],
// Selector de Dificultad
["div", { class : "xg-field-row" }, [
["span", { "data-i18n" : "opt_difficulty" }],
["select", {
class : "xg-select",
"data-i18n-without" : "true",
onchange : (select) => {
xgravity.settings.add({ current_difficulty : select.value });
}
}, [
["option", { value : "training", ...(current_diff === "training" ? { selected : "selected" } : {}), "data-i18n" : "diff_training" }],
["option", { value : "newbie", ...(current_diff === "newbie" ? { selected : "selected" } : {}), "data-i18n" : "diff_newbie" }],
["option", { value : "lazy", ...(current_diff === "lazy" ? { selected : "selected" } : {}), "data-i18n" : "diff_lazy" }],
["option", { value : "average", ...(current_diff === "average" ? { selected : "selected" } : {}), "data-i18n" : "diff_average" }],
["option", { value : "hard", ...(current_diff === "hard" ? { selected : "selected" } : {}), "data-i18n" : "diff_hard" }],
["option", { value : "berserk", ...(current_diff === "berserk" ? { selected : "selected" } : {}), "data-i18n" : "diff_berserk" }],
["option", { value : "insane", ...(current_diff === "insane" ? { selected : "selected" } : {}), "data-i18n" : "diff_insane" }]
]]
]],
// Calidad de Canvas
["div", { class : "xg-field-row" }, [
["label", {
id : "xg_lbl_quality",
"data-i18n" : "opt_canvas_quality"
}, xgravity.i18n.get("opt_canvas_quality", { quality : current_quality })],
["input", {
type : "range",
class : "xg-range",
min : "3.125",
max : "100",
step : "3.125",
value : current_quality,
"data-i18n-without" : "true",
oninput : (input) => {
const value = parseFloat(input.value);
xgravity.settings.add({ canvas_quality : value });
const lbl = document.getElementById("xg_lbl_quality");
if(lbl)
lbl.textContent = xgravity.i18n.get("opt_canvas_quality", { quality : value });
}
}]
]],
// Modo de Fondo
["div", { class : "xg-field-row" }, [
["span", { "data-i18n" : "opt_background" }],
["select", {
class : "xg-select",
"data-i18n-without" : "true",
onchange : (select) => {
xgravity.settings.add({ background_mode : select.value });
}
}, [
["option", { value : "image", ...(current_bg_mode === "image" ? { selected : "selected" } : {}), "data-i18n" : "opt_bg_image" }],
["option", { value : "stars", ...(current_bg_mode === "stars" ? { selected : "selected" } : {}), "data-i18n" : "opt_bg_stars" }],
["option", { value : "procedural", ...(current_bg_mode === "procedural" ? { selected : "selected" } : {}), "data-i18n" : "opt_bg_procedural" }]
]]
]],
// Tecla de Acción
["div", { class : "xg-field-row" }, [
["span", { "data-i18n" : "opt_action_key" }],
["button", {
id : "xg_btn_key",
class : "xg-btn",
"data-i18n-without" : "true",
onclick : (btn) => {
if(listening_key) return;
listening_key = true;
btn.textContent = xgravity.i18n.get("btn_press_key");
const on_keydown = (e) => {
window.removeEventListener("keydown", on_keydown);
const chosen = e.code === "Space" ? "Space" : (e.key.length === 1 ? e.key.toUpperCase() : e.code);
xgravity.settings.add({ action_key : chosen });
btn.textContent = chosen;
listening_key = false;
};
window.addEventListener("keydown", on_keydown);
}
}, current_action_key]
]],
// Botón Volver con auto-limpieza
["button", {
class : "xg-btn",
"data-i18n" : "btn_back",
onclick : () => {
self.close();
if(xgravity.views.main_menu)
xgravity.views.main_menu.go();
}
}]
]]
]
]);
view_element = nodes[0];
xgravity.i18n.update();
};
};
return OptionsView;
})();

View File

@ -0,0 +1,45 @@
"use strict";
const RankingView = (function(){
const RankingView = function(xgravity){
const self = this;
let view_element = null;
this.close = () => {
if(view_element && view_element.parentNode){
view_element.parentNode.removeChild(view_element);
view_element = null;
}
};
this.go = () => {
self.close();
const nodes = xgravity.html(xgravity.item_self, [
"div", { class : "xg-view xg-ranking" }, [
["h2", { class : "xg-subtitle", "data-i18n" : "rank_title" }],
["div", { class : "xg-panel" }, [
["p", { "data-i18n" : "rank_empty" }],
["button", {
class : "xg-btn",
"data-i18n" : "btn_back",
onclick : () => {
self.close();
if(xgravity.views.main_menu)
xgravity.views.main_menu.go();
}
}]
]]
]
]);
view_element = nodes[0];
xgravity.i18n.update();
};
};
return RankingView;
})();

61
Public/images/logo.svg Normal file
View File

@ -0,0 +1,61 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
<defs>
<!-- Degradado base para la estructura de la X -->
<linearGradient id="xg-cyan-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#bae6fd" />
<stop offset="45%" stop-color="#38bdf8" />
<stop offset="100%" stop-color="#0284c7" />
</linearGradient>
<!-- Degradado del núcleo de gravedad / singularidad -->
<radialGradient id="xg-core-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="25%" stop-color="#7dd3fc" />
<stop offset="60%" stop-color="#0284c7" />
<stop offset="100%" stop-color="#030712" stop-opacity="0" />
</radialGradient>
<!-- Resplandor exterior táctico -->
<radialGradient id="xg-back-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#0284c7" stop-opacity="0.35" />
<stop offset="100%" stop-color="#030712" stop-opacity="0" />
</radialGradient>
</defs>
<!-- 1. Fondo contenedor oscuro estilo chasis / badge (para contraste en cualquier pestaña) -->
<rect width="512" height="512" rx="112" fill="#030712" stroke="#1e293b" stroke-width="8" />
<!-- 2. Halo ambiental de gravedad profunda -->
<circle cx="256" cy="256" r="220" fill="url(#xg-back-glow)" />
<!-- 3. Retícula HUD: Esquinas tácticas militares -->
<path d="M 64 112 L 64 64 L 112 64" fill="none" stroke="#38bdf8" stroke-width="8" stroke-linecap="round" opacity="0.65" />
<path d="M 400 64 L 448 64 L 448 112" fill="none" stroke="#38bdf8" stroke-width="8" stroke-linecap="round" opacity="0.65" />
<path d="M 448 400 L 448 448 L 400 448" fill="none" stroke="#38bdf8" stroke-width="8" stroke-linecap="round" opacity="0.65" />
<path d="M 112 448 L 64 448 L 64 400" fill="none" stroke="#38bdf8" stroke-width="8" stroke-linecap="round" opacity="0.65" />
<!-- 4. Órbita elíptica secundaria (inclinada hacia la derecha) -->
<ellipse cx="256" cy="256" rx="200" ry="64" fill="none" stroke="#0284c7" stroke-width="6" stroke-dasharray="14 12 36 12" transform="rotate(45 256 256)" opacity="0.45" />
<!-- 5. Aspas principales en "X" (Diseño cinético afilado) -->
<!-- Brazo 1: Superior Izquierda -> Inferior Derecha -->
<polygon points="106,126 182,96 406,386 330,416" fill="url(#xg-cyan-grad)" />
<!-- Brazo 2: Superior Derecha -> Inferior Izquierda -->
<polygon points="406,126 330,96 106,386 182,416" fill="url(#xg-cyan-grad)" />
<!-- Muescas aerodinámicas de cabina en los extremos -->
<polygon points="182,96 214,142 150,154" fill="#030712" opacity="0.4" />
<polygon points="330,96 298,142 362,154" fill="#030712" opacity="0.4" />
<polygon points="330,416 298,370 362,358" fill="#030712" opacity="0.4" />
<polygon points="182,416 214,370 150,358" fill="#030712" opacity="0.4" />
<!-- 6. Órbita ecuatorial gravitatoria primaria (anillo de distorsión) -->
<ellipse cx="256" cy="256" rx="215" ry="74" fill="none" stroke="#38bdf8" stroke-width="12" stroke-linecap="round" stroke-dasharray="24 14 80 14" transform="rotate(-30 256 256)" />
<!-- 7. Pozo de singularidad central (Círculo de corte oscuro) -->
<circle cx="256" cy="256" r="68" fill="#030712" stroke="#38bdf8" stroke-width="10" />
<!-- 8. Núcleo cuántico hiperbrillante central -->
<circle cx="256" cy="256" r="44" fill="url(#xg-core-glow)" />
<circle cx="256" cy="256" r="14" fill="#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

93
Public/index.html Normal file
View File

@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="es">
<head>
<title data-i18n="xgravity">XGravity</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="./images/logo.svg">
<meta name="xdoc:link" content="https://xgravity.k3y.pw/" />
<meta name="xdoc:git" content="https://git.k3y.pw/KyMAN/XGravity" />
<meta name="xdoc:authors" content="KyMAN" />
<meta name="xdoc:ai-authors" content="Gemini" />
<meta name="xdoc:since" content="20261014" />
<meta name="xdoc:version" content="20261015" />
<meta name="xdoc:license" content=" Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International" />
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./data/i18n.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./data/settings.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./data/styles.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Utils/Styles.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Utils/Common.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Utils/I18N.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Utils/Settings.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<!-- Enemies -->
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/StaticMineEnemyModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/BouncerEnemyModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/ChaserEnemyModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/RusherEnemyModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/AsteroidEnemyModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/AsteroidThrowerEnemyModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Fx/ShieldExplosionsFx.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Fx/HullHitExplosionsFx.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Fx/DeflectionSparksFx.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Fx/CockpitImpactFx.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<!-- Strategies -->
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Strategies/SpawnStrategies.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Strategies/BehaviorStrategies.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/SettingsManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/I18NManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/AudioManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/EnemiesManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/ExplosionsManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<!-- Backgrounds -->
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Backgrounds/ImageBackground.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Backgrounds/StarsBackground.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Backgrounds/ProceduralBackground.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<!-- Models -->
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/CockpitModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/DroneModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/TelemetryHudModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<!-- <script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/EnemiesModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script> -->
<!-- <script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/ExplosionsModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script> -->
<!-- Sounds -->
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Sounds/ShieldExplosionSound.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Sounds/HitExplosionSound.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Sounds/ShieldLowAlarmSound.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Sounds/PerimeterWarningSound.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Sounds/MothershipImpactSound.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Sounds/AsteroidThreatAlarmSound.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<!-- Views -->
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/MainMenuView.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/DifficultySelectView.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/OptionsView.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/RankingView.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/CreditsView.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/FpsOverlayView.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/GameView.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/GameOverView.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Application/Values.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Application/XGravity.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" charset="utf-8">
"use strict";
const xgravity = new XGravity();
</script>
</head>
<body></body>
</html>

13
Public/md/ai-dev-rules.md Normal file
View File

@ -0,0 +1,13 @@
El proyecto tiene un raíz privado con los archivos `LICENSE`, `README.md` y `.gitignore`; luego tiene un lado público el cual es `/Public`. Dentro de éste un `index.html` y los directorios `data` y `ecma`. En el directorio `data` se meterá todo lo que no se meta en el repositorio de Git, como configuraciones personalizadas, la base de datos, etc. Mientras que los archivos de Script se meten dentro de `ecma` el cual tiene la estructura `Application` para los archivos que funcionen sobre el proyecto de forma diferente o independiente; `Utils` para archivos totalmente autónomos; `Managers` para gestores como `SettingsManager` o `I18NManager`; `Views` para las vistas; y `Models` para los modelos si hicieren falta.
Las reglas de desarrollo son:
- La sangría es de 4 espacios.
- Las variables y nombres de funciones, métodos y atributos son Snake Lower Case; el nombre de las clases es en Pascal Case; las constantes de clase o constantes sueltas estarán en Snake Upper Case.
- El GUI HTML se basa en un sistema que simula DPIs a partir de la unidad de medida `em`, el cual divide el lado más estrecho del GUI en 40 celdas por defecto. `1em` es equivalente a una celda. Cada celda es cuadrada.
- Todas las clases que se generen han de ser Funciones Constructoras. Los métodos y atributos estáticos tienen que ir vinculados al nombre sin `prototype`.
- Todo valor susceptible de ser de configuración ha de estar integrado en el archivo `/Public/ecma/Utils/Settings.ecma.js`.
- En HTML, todos los elementos que contengan textos internacionalizados han de llevar el atributo `data-i18n` con la clave del texto, y en caso de ser campos sin contenido como `input`, `textarea`, etc. Han de llevar a mayores el atributo `data-i18n-without="true"` para indicar que no se añada la traducción a su contenido, sólo a los atributos, si los tuviere, como `title`, `placeholder`, etc.
- Las vistas estará en el directorio `/Public/ecma/Views` y tendrán como nombre `NombreDeVistaView.ecma.js` y nombre de clase función constructora `NombreDeVistaView` y se implementarán en el objeto `xgravity.views` con el nombre `nombre_de_vista`. Cada vista ha de tener un método `go` para construir y visualizar dicha vista cada vez que se realiza la acción de ir a dicha vista. Cada texto que se muestre tiene que recogerse del método `xgravity.i18n.get` y sus claves y valores en el archivo `/Public/ecma/Utils/I18N.ecma.js` en cada uno de los idiomas deseados.
- Cualquier HTML que se haga, salvo que no se pueda realizar por dicho método, ha de estar hecho con el método `xgravity.html`.
- Los estilos han de agregarse por bloques que se deseen al archivo `/Public/ecma/Utils/Styles.ecma.js` en formato del método `Common.SCSS`.

470
Public/md/base-dev.md Normal file
View File

@ -0,0 +1,470 @@
Tengo la siguiente base estructural hecha sobre la que trabajar. Para empezar, Tenemos el archivo principal de la aplicación, el cual es `/Public/ecma/Application/XGravity.ecma.js`.
```js
"use strict";
const XGravity = (function(){
const XGravity = function(){
const self = this,
screen = {x : 0, y : 0};
let cells = 40,
thread_interval = null,
fps = 12;
this.item_self = document;
this.settings = new SettingsManager(self);
this.i18n = new I18NManager(self);
this.views = {};
const constructor = () => {
const position = self.settings.get("position", null, "body");
let target, preload = setInterval(() => {
if(target = document.querySelector(position)){
clearInterval(preload);
self.item_self = target.appendChild(document.createElement("div"));
self.item_self.className = self.item_self.id = self.settings.get("class_name", null, "xgravity");
self.item_self.setAttribute("data-cells", cells = self.settings.get("cells", null, cells));
thread_interval = setInterval(thread, 1000 / fps);
build();
};
}, fps = self.settings.get(["frames_per_second", "fps"], null, fps));
Common.SCSS(SCSS);
};
const thread = () => {
if(screen.x != self.item_self.offsetWidth || screen.y != self.item_self.offsetHeight){
screen.x = self.item_self.offsetWidth;
screen.y = self.item_self.offsetHeight;
self.item_self.style.fontSize = ((screen.x < screen.y ? screen.x : screen.y) / cells) + "px";
};
};
const build = () => {};
/**
* @param {...(HTMLElement|string|Array.<any|null>)} inputs
* @returns {Array.<HTMLElement>}
* @access public
* @static
*/
this.html = (...inputs) => {
const fragment = new DocumentFragment(),
master = (
inputs[0] && (inputs[0].tagName || inputs[0].nodeName) ? inputs[0] :
typeof inputs[0] == "string" ? document.querySelector(inputs[0]) :
null),
items = [];
(master ? inputs.slice(1) : inputs).forEach(subinputs => {
if(subinputs && (subinputs.tagName || subinputs.nodeName))
fragment.appendChild(subinputs);
else if(typeof subinputs == "string")
fragment.appendChild(document.createTextNode(subinputs));
else if(subinputs instanceof Array){
const [tag, attributes, children] = subinputs.concat([null, null]).slice(0, 3),
item = document.createElement(tag);
attributes && attributes.constructor == Object && Object.entries(attributes).forEach(([key, value]) => {
if(/^on[_\-]?/i.test(key) && typeof value == "function")
item.addEventListener(key.replace(/^on[_\-]?/i, "").replace(/[^a-z0-9]+/gi, "").toLowerCase(), event => {
value(item, event);
});
else if(value !== null)
item.setAttribute(key.replace(/[^a-z0-9]+/gi, "-"), value);
});
if(children instanceof Array)
self.html(...children).forEach(child => item.appendChild(child));
else if(typeof children == "string")
item.innerHTML += children;
fragment.appendChild(item);
};
});
items.push(...fragment.childNodes);
master && master.appendChild(fragment);
return items;
};
constructor();
};
return XGravity;
})();
```
Dentro de este hay dos gestores, siendo el primero de ellos el de configuración `/Public/ecma/Managers/SettingsManager.ecma.js`:
```js
"use strict";
const SettingsManager = (function(){
const SettingsManager = function(xgravity){
const self = this,
settings = {};
const constructor = () => {
self.add(settings, true);
};
this.add = (block, overwrite = true) => {
for(const key in block)
if(overwrite || settings[key] === undefined)
settings[key] = block[key];
};
this.get = (keys, inputs = null, _default = null) => Common.get_value(keys, [inputs, settings], _default);
constructor();
};
return SettingsManager;
})();
```
Y el segundo el de gestión de textos e idiomas `/Public/ecma/Managers/I18NManager.ecma.js`:
```js
"use strict";
const I18NManager = (function(){
const I18NManager = function(xgravity){
const self = this,
sentences = {};
let language = "espanol";
const constructor = () => {
[i18n, typeof custom_i18n != "undefined" ? custom_i18n : {}].forEach(block => {
self.add(block, true);
});
self.set_language(xgravity.settings.get("language"), null, Object.keys(sentences)[0] || language);
};
this.add = (new_sentences, overwrite = true) => {
for(const language in new_sentences){
if(!sentences[language])
sentences[language] = {};
for(const key in new_sentences[language])
if(overwrite || sentences[language][key] === undefined)
sentences[language][key] = new_sentences[language][key];
};
};
const get_sentence = (strings, _default) => {
const done = [];
if(typeof strings == "string")
strings = [strings];
for(const current of [language].concat(Object.keys(sentences)))
if(!done.includes(current)){
done.push(current);
for(const key of strings)
if(sentences[current][key] !== undefined)
return sentences[current][key];
};
if(_default !== null)
return _default;
for(const text of strings)
return text;
return "";
};
this.get = (strings, inputs = null, _default = null) => Common.string_variables(get_sentence(strings, _default), inputs);
this.update = () => {
document.querySelectorAll("[data-i18n]").forEach(element => {
const text = self.get(element.getAttribute("data-i18n"));
if(element.getAttribute("data-i18n-without") != "true")
element.textContent = text;
["title", "alt", "placeholder"].forEach(key => {
element.hasAttribute(key) && element.setAttribute(key, text);
});
})
};
this.set_language = new_language => {
if(language != new_language && sentences[new_language]){
language = new_language;
self.update();
};
};
this.get_languages = () => Object.keys(sentences);
this.get_current_language = () => language;
constructor();
};
return I18NManager;
})();
```
Por otro lado, tenemos una librería con métodos comunes la cual es `/Public/ecma/Utils/Common.ecma.js`:
```js
"use strict";
const Common = (function(){
const Common = function(){};
Common.get_keys = (...items) => items.reduce((keys, item) => {
if(item){
if(typeof item == "string" && /^[a-z_][a-z0-9_]*$/i.test(item) && !keys.includes(item))
keys.push(item);
else if(item instanceof Array)
for(const key of Common.get_keys(...item))
keys.includes(item) || keys.push(key);
};
return keys;
}, []);
Common.get_dictionaries = (...items) => items.reduce((dictionaries, item) => {
if(item){
if(item.constructor == Object)
dictionaries.push(item);
else if(item instanceof Array)
dictionaries.push(...Common.get_dictionaries(...item));
};
return dictionaries;
}, []);
Common.get_value = (keys, inputs, _default = null) => {
if((keys = Common.get_keys(keys)).length)
for(const subinputs of Common.get_dictionaries(inputs))
for(const key of keys)
if(subinputs[key] !== undefined)
return subinputs[key];
return _default;
};
Common.get_dictionary = (items, overwrite = false) => {
if(items){
if(items.constructor == Object)
return items;
else if(items instanceof Array)
return items.reduce((dictionary, item) => {
Object.entries(Common.get_dictionary(item)).forEach(([key, value]) => {
if(overwrite || dictionary[key] === undefined)
dictionary[key] = value;
});
return dictionary;
}, {});
};
return {};
};
Common.string_variables = (string, variables, _default = null) => {
variables = Common.get_dictionary(variables);
return ("" + string).replace(/\{([^\{\}]+)\}/g, (all, key) => (
variables[key] !== undefined ? variables[key] :
_default !== null ? _default :
all));
};
Common.execute_array = (array, each_callback, end_callback, i = 0) => {
if(i == array.length)
end_callback();
else
each_callback(array[i], () => {
Common.execute_array(array, each_callback, end_callback, i + 1);
});
};
Common.SCSS = (data, selector = "") => {
/** @type {Arrya.<string>} */
const childs = [];
/** @type {string} */
let css = ``;
Object.entries(data).forEach(([key, value]) => {
if(value && value.constructor == Object)
key.split(",").forEach(key => {
/** @type {string} */
const subselector = `${(selector ? selector + (
key[0] == "&" ? "" : " "
) : "") + (
key[0] == "&" ? key.substring(1) : key
)}`,
/** @type {Array<string>} */
subcss = Common.SCSS(value, subselector);
childs.push(`\n\n${subselector}{${subcss[0]}\n}`, ...subcss.slice(1));
});
else
css += `\n ${key.replace(/[^a-z0-9]+/gi, "-")} : ${value === null ? "none" : value};`;
});
if(!selector){
/** @type {HTMLStyleElement} */
let style = document.querySelector("head>style");
if(!style){
style = document.querySelector("head").appendChild(document.createElement("style"));
[
["data-type", "text/css;charset=utf-8"],
["data-language", "CSS3"],
["charset", "utf-8"]
].forEach(([key, value]) => style.setAttribute(key, value === null ? "none" : value));
};
style.appendChild(document.createTextNode(css + childs.join("")));
};
return [css, ...childs];
};
return Common;
})();
```
Otra librería la cual es un diccionario para almacenar la configuración la cual es `/Public/ecma/Utils/Settings.ecma.js`:
```js
"use strict";
/** @type {Dictionary.<string, any|null>} */
const settings = {
cells : 40,
position : "body",
class_name : "xgravity",
frames_per_second : 12,
language : "espanol",
...(typeof custom_settings == "undefined" ? {} : custom_settings)
};
```
Otra librería la cual es un diccionario de diccionarios con los textos donde el primer nivel, la clave indica el idioma, y en el diccionario anidado, la clave identifica al texto internacionalizado `/Public/ecma/Utils/I18N.ecma.js`:
```js
"use strict";
/** @type {Dictionary.<string, Dictionary.<string, string>>} */
const i18n = {
espanol : {}
};
```
Por otro lado, tenemos la librería de estilos, que es un JS que simula una especie de SASS la cual es `/Public/ecma/Utils/Styles.ecma.js`:
```js
"use strict";
/** @type {Dictionary.<string, any|null>} */
const SCSS = {
...{
"body,html" : {
height : "100%",
margin : "0em"
}
},
...{
".xgravity" : {
position : "relative",
top : "0em",
left : "0em",
width : "100%",
height : "100%"
}
},
...(typeof custom_styles == "undefined" ? {} : custom_styles)
};
```
Por otro lado, tenemos un directorio que está excluído del proyecto mediante `/.gitignore` que es `/Public/data` donde metemos cosas personalizadas o que no queramos incluir en el mismo, como pueden ser bases de datos JS, Assets agenos, etc.
Finalmente tenemos el Index del proyecto el cual es `/Public/index.html`:
```html
<!DOCTYPE html>
<html lang="es">
<head>
<title data-i18n="xgravity">XGravity</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="./images/logo.svg">
<meta name="xdoc:link" content="https://xgravity.k3y.pw/" />
<meta name="xdoc:git" content="https://git.k3y.pw/KyMAN/XGravity" />
<meta name="xdoc:authors" content="KyMAN" />
<meta name="xdoc:ai-authors" content="Gemini" />
<meta name="xdoc:since" content="20261006" />
<meta name="xdoc:version" content="20261006" />
<meta name="xdoc:license" content=" Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International" />
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./data/database.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./data/i18n.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./data/settings.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./data/styles.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Utils/Styles.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Utils/Common.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Utils/I18N.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Utils/Settings.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/SettingsManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/I18NManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Application/XGravity.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" charset="utf-8">
"use strict";
const xgravity = new XGravity();
</script>
</head>
<body></body>
</html>
```