feat(ecma): Base game done.
This commit is contained in:
parent
5dcb86e982
commit
b892747b1a
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/Data
|
||||||
|
/Public/data
|
||||||
|
/Public/images/backgrounds
|
||||||
|
/Public/images/game
|
||||||
102
Public/ecma/Application/Values.ecma.js
Normal file
102
Public/ecma/Application/Values.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
145
Public/ecma/Application/XGravity.ecma.js
Normal file
145
Public/ecma/Application/XGravity.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
76
Public/ecma/Backgrounds/ImageBackground.ecma.js
Normal file
76
Public/ecma/Backgrounds/ImageBackground.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
276
Public/ecma/Backgrounds/ProceduralBackground.ecma.js
Normal file
276
Public/ecma/Backgrounds/ProceduralBackground.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
66
Public/ecma/Backgrounds/StarsBackground.ecma.js
Normal file
66
Public/ecma/Backgrounds/StarsBackground.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
87
Public/ecma/Fx/CockpitImpactFx.ecma.js
Normal file
87
Public/ecma/Fx/CockpitImpactFx.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
77
Public/ecma/Fx/DeflectionSparksFx.ecma.js
Normal file
77
Public/ecma/Fx/DeflectionSparksFx.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
134
Public/ecma/Fx/HullHitExplosionsFx.ecma.js
Normal file
134
Public/ecma/Fx/HullHitExplosionsFx.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
138
Public/ecma/Fx/ShieldExplosionsFx.ecma.js
Normal file
138
Public/ecma/Fx/ShieldExplosionsFx.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
119
Public/ecma/Managers/AudioManager.ecma.js
Normal file
119
Public/ecma/Managers/AudioManager.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
748
Public/ecma/Managers/EnemiesManager.ecma.js
Normal file
748
Public/ecma/Managers/EnemiesManager.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
70
Public/ecma/Managers/ExplosionsManager.ecma.js
Normal file
70
Public/ecma/Managers/ExplosionsManager.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
81
Public/ecma/Managers/I18NManager.ecma.js
Normal file
81
Public/ecma/Managers/I18NManager.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
40
Public/ecma/Managers/SettingsManager.ecma.js
Normal file
40
Public/ecma/Managers/SettingsManager.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
174
Public/ecma/Models/AsteroidEnemyModel.ecma.js
Normal file
174
Public/ecma/Models/AsteroidEnemyModel.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
112
Public/ecma/Models/AsteroidThrowerEnemyModel.ecma.js
Normal file
112
Public/ecma/Models/AsteroidThrowerEnemyModel.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
105
Public/ecma/Models/BouncerEnemyModel.ecma.js
Normal file
105
Public/ecma/Models/BouncerEnemyModel.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
111
Public/ecma/Models/ChaserEnemyModel.ecma.js
Normal file
111
Public/ecma/Models/ChaserEnemyModel.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
162
Public/ecma/Models/CockpitModel.ecma.js
Normal file
162
Public/ecma/Models/CockpitModel.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
442
Public/ecma/Models/DroneModel.ecma.js
Normal file
442
Public/ecma/Models/DroneModel.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
272
Public/ecma/Models/ExplosionsModel.ecma.js
Normal file
272
Public/ecma/Models/ExplosionsModel.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
105
Public/ecma/Models/RusherEnemyModel.ecma.js
Normal file
105
Public/ecma/Models/RusherEnemyModel.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
105
Public/ecma/Models/StaticMineEnemyModel.ecma.js
Normal file
105
Public/ecma/Models/StaticMineEnemyModel.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
442
Public/ecma/Models/TelemetryHudModel.ecma.js
Normal file
442
Public/ecma/Models/TelemetryHudModel.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
82
Public/ecma/Sounds/AsteroidThreatAlarmSound.ecma.js
Normal file
82
Public/ecma/Sounds/AsteroidThreatAlarmSound.ecma.js
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
52
Public/ecma/Sounds/HitExplosionSound.ecma.js
Normal file
52
Public/ecma/Sounds/HitExplosionSound.ecma.js
Normal 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
136
Public/ecma/Sounds/MothershipImpactSound.ecma.js
Normal file
136
Public/ecma/Sounds/MothershipImpactSound.ecma.js
Normal 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
51
Public/ecma/Sounds/PerimeterWarningSound.ecma.js
Normal file
51
Public/ecma/Sounds/PerimeterWarningSound.ecma.js
Normal 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
87
Public/ecma/Sounds/ShieldExplosionSound.ecma.js
Normal file
87
Public/ecma/Sounds/ShieldExplosionSound.ecma.js
Normal 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
94
Public/ecma/Sounds/ShieldLowAlarmSound.ecma.js
Normal file
94
Public/ecma/Sounds/ShieldLowAlarmSound.ecma.js
Normal 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
327
Public/ecma/Strategies/BehaviorStrategies.ecma.js
Normal file
327
Public/ecma/Strategies/BehaviorStrategies.ecma.js
Normal 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; }
|
||||||
|
};
|
||||||
|
})();
|
||||||
153
Public/ecma/Strategies/SpawnStrategies.ecma.js
Normal file
153
Public/ecma/Strategies/SpawnStrategies.ecma.js
Normal 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; }
|
||||||
|
};
|
||||||
|
})();
|
||||||
200
Public/ecma/Utils/Common.ecma.js
Normal file
200
Public/ecma/Utils/Common.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
78
Public/ecma/Utils/I18N.ecma.js
Normal file
78
Public/ecma/Utils/I18N.ecma.js
Normal 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"
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
390
Public/ecma/Utils/Settings.ecma.js
Normal file
390
Public/ecma/Utils/Settings.ecma.js
Normal 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)
|
||||||
|
};
|
||||||
392
Public/ecma/Utils/Styles.ecma.js
Normal file
392
Public/ecma/Utils/Styles.ecma.js
Normal 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)
|
||||||
|
};
|
||||||
80
Public/ecma/Views/CreditsView.ecma.js
Normal file
80
Public/ecma/Views/CreditsView.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
89
Public/ecma/Views/DifficultySelectView.ecma.js
Normal file
89
Public/ecma/Views/DifficultySelectView.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
78
Public/ecma/Views/FpsOverlayView.ecma.js
Normal file
78
Public/ecma/Views/FpsOverlayView.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
107
Public/ecma/Views/GameOverView.ecma.js
Normal file
107
Public/ecma/Views/GameOverView.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
683
Public/ecma/Views/GameView.ecma.js
Normal file
683
Public/ecma/Views/GameView.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
101
Public/ecma/Views/MainMenuView.ecma.js
Normal file
101
Public/ecma/Views/MainMenuView.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
150
Public/ecma/Views/OptionsView.ecma.js
Normal file
150
Public/ecma/Views/OptionsView.ecma.js
Normal 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;
|
||||||
|
})();
|
||||||
45
Public/ecma/Views/RankingView.ecma.js
Normal file
45
Public/ecma/Views/RankingView.ecma.js
Normal 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
61
Public/images/logo.svg
Normal 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
93
Public/index.html
Normal 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
13
Public/md/ai-dev-rules.md
Normal 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
470
Public/md/base-dev.md
Normal 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>
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue
Block a user