#wip(ecma): Building elevators.
This commit is contained in:
parent
b45bbaac2f
commit
90a5631fd9
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/Public/data
|
||||
207
Public/ecma/Application/HollowBond.ecma.js
Normal file
207
Public/ecma/Application/HollowBond.ecma.js
Normal file
@ -0,0 +1,207 @@
|
||||
"use strict";
|
||||
|
||||
const HollowBond = (function(){
|
||||
|
||||
const HollowBond = function(inputs = null){
|
||||
|
||||
const self = this,
|
||||
screen = {x : 0, y : 0};
|
||||
let cells = 40,
|
||||
thread_interval = null,
|
||||
ids = {};
|
||||
|
||||
this.item_self = document;
|
||||
this.fps = 60;
|
||||
this.seed = 0;
|
||||
this.render_scale = 100;
|
||||
this.show_fps = true;
|
||||
this.gravity = 9.81;
|
||||
this.chunk_size = 50;
|
||||
this.chunk_frame_probability = .91;
|
||||
this.chunk_frame_step = 10;
|
||||
this.chunk_frame_maximum_gap = 25;
|
||||
this.maximum_chunks = 0;
|
||||
this.room_minimum_size = 2;
|
||||
this.room_maximum_size = 25;
|
||||
this.room_size_decay = .94;
|
||||
this.room_size_grow = .91;
|
||||
this.room_height = 3;
|
||||
this.room_minimum_distance_hole = 2;
|
||||
this.rooms_hole_probability_same = .9;
|
||||
this.room_padding = .1;
|
||||
this.room_free_cuts_minimum = 4;
|
||||
this.room_free_cuts_maximum = 12;
|
||||
this.frame_wall_probability = .35;
|
||||
this.frame_wall_decay = .8;
|
||||
this.frame_height = 2;
|
||||
this.frame_width = .8;
|
||||
this.frame_padding = .05;
|
||||
this.hole_seam_tries = 5;
|
||||
this.hole_minimum_size = 4;
|
||||
this.hole_maximum_size = 20;
|
||||
this.hole_minimum_height = 2;
|
||||
this.hole_maximum_height = 25;
|
||||
this.hole_size_decay = .75;
|
||||
this.hole_height_decay = .85;
|
||||
this.hole_margin = 2;
|
||||
this.hole_floor_step = 3;
|
||||
this.internal_hole_probability = .55;
|
||||
this.elevator_minimum_floors = 3;
|
||||
this.elevator_maximum_floors = 70;
|
||||
this.elevator_floors_decay = .95;
|
||||
this.elevator_size = 2;
|
||||
this.elevator_height = 2;
|
||||
this.elevator_padding = .1;
|
||||
this.elevator_margin_border = 2;
|
||||
this.elevator_margin_hole = 2;
|
||||
this.elevator_networks_count = 3;
|
||||
this.stairs_length = 4;
|
||||
this.stairs_width = 1;
|
||||
this.stairs_height = 2;
|
||||
this.stairs_padding = -1;
|
||||
this.stairs_probability = .9;
|
||||
this.horizontal_loaded_rooms_levels = 3;
|
||||
this.horizontal_unloaded_rooms_level = 5;
|
||||
|
||||
this.settings = new SettingsManager(this);
|
||||
this.i18n = new I18NManager(this);
|
||||
this.hash = new HashManager(this);
|
||||
this.builders = {
|
||||
frames : new FramesBuilder(this),
|
||||
stairs : new StairsBuilder(this),
|
||||
holes : new HolesBuilder(this),
|
||||
elevators : new ElevatorsBuilder(this),
|
||||
rooms : new RoomsBuilder(this)
|
||||
};
|
||||
this.map = new MapManager(this);
|
||||
this.themes = new ThemeManager(this);
|
||||
this.controls = {
|
||||
debug : new DebugControls(self),
|
||||
game : new GameControls(self)
|
||||
};
|
||||
this.inspector = new InspectorManager(this);
|
||||
this.dungeon = new DungeonManager(this);
|
||||
this.views = {
|
||||
menu : new MenuView(this),
|
||||
new_game : new NewGameView(this),
|
||||
options : new OptionsView(this),
|
||||
credits : new CreditsView(this),
|
||||
fps : new FpsView(this)
|
||||
};
|
||||
|
||||
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", inputs, "hollow-bond");
|
||||
self.item_self.setAttribute("data-cells", cells = self.settings.get("cells", inputs, cells));
|
||||
|
||||
thread_interval = setInterval(thread, 1000 / self.fps);
|
||||
|
||||
build();
|
||||
|
||||
};
|
||||
}, self.fps = self.settings.get(["frames_per_second", "fps"], inputs, self.fps));
|
||||
|
||||
self.render_scale = self.settings.get("render_scale", inputs, self.render_scale);
|
||||
self.show_fps = self.settings.get("show_fps", inputs, self.show_fps);
|
||||
self.chunk_size = self.settings.get("chunk_size", inputs, self.chunk_size);
|
||||
self.chunk_frame_probability = self.settings.get("chunk_frame_probability", inputs, self.chunk_frame_probability);
|
||||
self.chunk_frame_step = self.settings.get("chunk_frame_step", inputs, self.chunk_frame_step);
|
||||
self.chunk_frame_maximum_gap = self.settings.get("chunk_frame_maximum_gap", inputs, self.chunk_frame_maximum_gap);
|
||||
self.maximum_chunks = self.settings.get("maximum_chunks", inputs, self.maximum_chunks);
|
||||
self.room_height = self.settings.get("room_height", inputs, self.room_height);
|
||||
self.room_padding = self.settings.get("room_padding", inputs, self.room_padding);
|
||||
self.room_minimum_size = self.settings.get("room_minimum_size", inputs, self.room_minimum_size);
|
||||
self.room_maximum_size = self.settings.get("room_maximum_size", inputs, self.room_maximum_size);
|
||||
self.frame_height = self.settings.get("frame_height", inputs, self.frame_height);
|
||||
self.frame_width = self.settings.get("frame_width", inputs, self.frame_width);
|
||||
self.hole_maximum_height = self.settings.get("hole_maximum_height", inputs, self.hole_maximum_height);
|
||||
self.hole_minimum_height = self.settings.get("hole_minimum_height", inputs, self.hole_minimum_height);
|
||||
self.hole_maximum_size = self.settings.get("hole_maximum_size", inputs, self.hole_maximum_size);
|
||||
self.hole_minimum_size = self.settings.get("hole_minimum_size", inputs, self.hole_minimum_size);
|
||||
self.hole_seam_tries = self.settings.get("hole_seam_tries", inputs, self.hole_seam_tries);
|
||||
self.hole_size_decay = self.settings.get("hole_size_decay", inputs, self.hole_size_decay);
|
||||
self.hole_height_decay = self.settings.get("hole_height_decay", inputs, self.hole_height_decay);
|
||||
self.internal_hole_probability = self.settings.get("internal_hole_probability", inputs, self.internal_hole_probability);
|
||||
|
||||
|
||||
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 = () => {
|
||||
self.set_seed(self.settings.get("default_seed", null, null));
|
||||
self.views.menu.go();
|
||||
};
|
||||
|
||||
this.set_seed = seed => {
|
||||
self.seed = (
|
||||
seed === null || seed === undefined ? Common.random() :
|
||||
typeof seed == "string" ? Common.string_to_integer(seed) :
|
||||
seed | 0);
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
this.get_id = (name = "item") => `${name}_${ids[name] = (ids[name] || 0) + 1}`;
|
||||
|
||||
constructor();
|
||||
|
||||
};
|
||||
|
||||
return HollowBond;
|
||||
})();
|
||||
224
Public/ecma/Builders/ElevatorsBuilder.ecma.js
Normal file
224
Public/ecma/Builders/ElevatorsBuilder.ecma.js
Normal file
@ -0,0 +1,224 @@
|
||||
"use strict";
|
||||
|
||||
const ElevatorsBuilder = (function(){
|
||||
|
||||
const ElevatorsBuilder = function(hollow_bond){
|
||||
|
||||
const is_near_hole = (chunk, xf, yf, zf, xt, yt, zt) => {
|
||||
|
||||
const area_xf = xf - hollow_bond.elevator_margin_hole,
|
||||
area_xt = xt + hollow_bond.elevator_margin_hole,
|
||||
area_zf = zf - hollow_bond.elevator_margin_hole,
|
||||
area_zt = zt + hollow_bond.elevator_margin_hole;
|
||||
|
||||
for(const box of chunk.occupancy)
|
||||
if(box.type == "hole")
|
||||
if(yf < box.yt && yt > box.yf && area_xf < box.xt && area_xt > box.xf && area_zf < box.zt && area_zt > box.zf)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const is_clearance_free = (chunk, x, y, z, facing, clearance_depth) => {
|
||||
|
||||
const size = hollow_bond.elevator_size || 2;
|
||||
let cxf = x,
|
||||
cxt = x + size,
|
||||
czf = z,
|
||||
czt = z + size;
|
||||
|
||||
switch(facing){
|
||||
case "north":
|
||||
czt = z;
|
||||
czf = z - clearance_depth;
|
||||
break;
|
||||
case "south":
|
||||
czf = z + size;
|
||||
czt = czf + clearance_depth;
|
||||
break;
|
||||
case "west":
|
||||
cxt = x;
|
||||
cxf = x - clearance_depth;
|
||||
break;
|
||||
case "east":
|
||||
cxf = x + size;
|
||||
cxt = cxf + clearance_depth;
|
||||
break;
|
||||
};
|
||||
|
||||
return (
|
||||
!(cxf < chunk.xf || cxt > chunk.xt || czf < chunk.zf || czt > chunk.zt) &&
|
||||
!is_near_hole(chunk, cxf, y, czf, cxt, y + 1, czt, 0) &&
|
||||
!chunk.is_occupied(cxf, y, czf, cxt, y + 1, czt)
|
||||
);
|
||||
};
|
||||
|
||||
const add = (chunk, inputs) => {
|
||||
|
||||
const id = ElevatorModel.get_id(inputs.x, inputs.y, inputs.z),
|
||||
elevator = (
|
||||
chunk.elevators[id] ||
|
||||
hollow_bond.map.elevators[id] ||
|
||||
new ElevatorModel(hollow_bond, Object.assign({}, inputs))
|
||||
);
|
||||
|
||||
chunk.elevators[id] = elevator;
|
||||
|
||||
chunk.occupy(
|
||||
elevator.x,
|
||||
elevator.y,
|
||||
elevator.z,
|
||||
elevator.x + hollow_bond.elevator_size,
|
||||
elevator.y + 1,
|
||||
elevator.z + hollow_bond.elevator_size,
|
||||
"elevator",
|
||||
elevator.group_id
|
||||
);
|
||||
|
||||
return elevator;
|
||||
};
|
||||
|
||||
this.generate_seam = chunk => {
|
||||
|
||||
[[chunk.yf, "down"], [chunk.yt, "up"]].forEach(([plane_y, neighbor]) => {
|
||||
if(!chunk.neighbors[neighbor])
|
||||
return;
|
||||
|
||||
const networks = Math.floor(hollow_bond.hash.seam_elevator(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, 0) * hollow_bond.elevator_networks_count); // e.g. 0 a 2 redes por frontera
|
||||
|
||||
for(let net_i = 0; net_i < networks; net_i ++){
|
||||
|
||||
const group_id = `net_${chunk.xf}_${plane_y}_${chunk.zf}_${net_i}`,
|
||||
stops_count = Common.random_decay(
|
||||
hollow_bond.hash.seam_elevator(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, net_i * 10 + 1),
|
||||
hollow_bond.elevator_minimum_floors,
|
||||
hollow_bond.elevator_maximum_floors,
|
||||
hollow_bond.elevator_floors_decay
|
||||
),
|
||||
network_stops = [];
|
||||
|
||||
for(let stop_i = 0; stop_i < stops_count; stop_i ++){
|
||||
|
||||
const hash_offset = net_i * 100 + stop_i * 6,
|
||||
floor_y = plane_y - 25 + Math.floor(hollow_bond.hash.seam_elevator(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, hash_offset + 2) * 50),
|
||||
available_x = hollow_bond.chunk_size - hollow_bond.elevator_size - hollow_bond.elevator_margin_border * 2,
|
||||
available_z = hollow_bond.chunk_size - hollow_bond.elevator_size - hollow_bond.elevator_margin_border * 2;
|
||||
|
||||
if(available_x <= 0 || available_z <= 0)
|
||||
continue;
|
||||
|
||||
const elevator_x = chunk.xf + hollow_bond.elevator_margin_border + Math.floor(hollow_bond.hash.seam_elevator(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, hash_offset + 3) * available_x),
|
||||
elevator_z = chunk.zf + hollow_bond.elevator_margin_border + Math.floor(hollow_bond.hash.seam_elevator(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, hash_offset + 4) * available_z),
|
||||
facing = ["north", "south", "east", "west"][Math.floor(hollow_bond.hash.seam_elevator(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, hash_offset + 5) * 4)];
|
||||
|
||||
network_stops.push({
|
||||
group_id : group_id,
|
||||
x : elevator_x,
|
||||
y : floor_y,
|
||||
z : elevator_z,
|
||||
facing : facing,
|
||||
floor : stop_i
|
||||
});
|
||||
|
||||
floor_y >= chunk.yf && floor_y < chunk.yt &&
|
||||
!is_near_hole(chunk, elevator_x, floor_y, elevator_z, elevator_x + hollow_bond.elevator_size, floor_y + 1, elevator_z + hollow_bond.elevator_size, hollow_bond.elevator_margin_hole) &&
|
||||
!chunk.is_occupied(elevator_x, floor_y, elevator_z, elevator_x + hollow_bond.elevator_size, floor_y + 1, elevator_z + hollow_bond.elevator_size) &&
|
||||
is_clearance_free(chunk, elevator_x, floor_y, elevator_z, facing, hollow_bond.room_minimum_size) &&
|
||||
add(chunk, {
|
||||
group_id : group_id,
|
||||
x : elevator_x,
|
||||
y : floor_y,
|
||||
z : elevator_z,
|
||||
facing : facing,
|
||||
floors_from : Math.min(...network_stops),
|
||||
floors_count : network_stops.length,
|
||||
stops : network_stops,
|
||||
floor : stop_i
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
return chunk.elevators;
|
||||
};
|
||||
|
||||
this.generate_internal = chunk => {
|
||||
|
||||
const networks_count = 1 + Math.floor(hollow_bond.hash.elevator(chunk.xf, chunk.yf, chunk.zf, chunk.xt, chunk.yt, chunk.zt, 100) * (hollow_bond.elevator_internal_networks || 3));
|
||||
|
||||
for(let net = 0; net < networks_count; net ++){
|
||||
|
||||
const net_base_i = 200 + (net * 50),
|
||||
stops_count = Common.random_decay(
|
||||
hollow_bond.hash.elevator(chunk.xf, chunk.yf, chunk.zf, chunk.xt, chunk.yt, chunk.zt, net_base_i),
|
||||
hollow_bond.elevator_minimum_floors,
|
||||
hollow_bond.elevator_maximum_floors,
|
||||
hollow_bond.elevator_floors_decay
|
||||
),
|
||||
group_id = `el_internal_${chunk.x}_${chunk.y}_${chunk.z}_${net}`,
|
||||
group_cabins = [],
|
||||
available_x = hollow_bond.chunk_size - hollow_bond.elevator_size - hollow_bond.elevator_margin_border * 2,
|
||||
available_z = hollow_bond.chunk_size - hollow_bond.elevator_size - hollow_bond.elevator_margin_border * 2,
|
||||
available_y = hollow_bond.chunk_size - 2;
|
||||
|
||||
if(available_x <= 0 || available_z <= 0 || available_y <= 0)
|
||||
continue;
|
||||
|
||||
for(let stops = 0; stops < stops_count; stops ++){
|
||||
|
||||
const cabin_i = net_base_i + 1 + stops * 5,
|
||||
elevator_x = chunk.xf + hollow_bond.elevator_margin_border + Math.floor(hollow_bond.hash.elevator(chunk.xf, chunk.yf, chunk.zf, chunk.xt, chunk.yt, chunk.zt, cabin_i + 1) * available_x),
|
||||
elevator_y = chunk.yf + 1 + Math.floor(hollow_bond.hash.elevator(chunk.xf, chunk.yf, chunk.zf, chunk.xt, chunk.yt, chunk.zt, cabin_i + 2) * available_y),
|
||||
elevator_z = chunk.zf + hollow_bond.elevator_margin_border + Math.floor(hollow_bond.hash.elevator(chunk.xf, chunk.yf, chunk.zf, chunk.xt, chunk.yt, chunk.zt, cabin_i + 3) * available_z),
|
||||
facing = ["north", "south", "east", "west"][Math.floor(hollow_bond.hash.elevator(chunk.xf, chunk.yf, chunk.zf, chunk.xt, chunk.yt, chunk.zt, cabin_i + 4) * 4)];
|
||||
|
||||
if(
|
||||
is_near_hole(chunk, elevator_x, elevator_y, elevator_z, elevator_x + hollow_bond.elevator_size, elevator_y + 1, elevator_z + hollow_bond.elevator_size, hollow_bond.elevator_margin_hole) ||
|
||||
chunk.is_occupied(elevator_x, elevator_y, elevator_z, elevator_x + hollow_bond.elevator_size, elevator_y + 1, elevator_z + hollow_bond.elevator_size) ||
|
||||
!is_clearance_free(chunk, elevator_x, elevator_y, elevator_z, facing, hollow_bond.room_minimum_size)
|
||||
)
|
||||
continue;
|
||||
|
||||
chunk.occupy(elevator_x, elevator_y, elevator_z, elevator_x + hollow_bond.elevator_size, elevator_y + 1, elevator_z + hollow_bond.elevator_size, "elevator", group_id);
|
||||
|
||||
group_cabins.push({
|
||||
x : elevator_x,
|
||||
y : elevator_y,
|
||||
z : elevator_z,
|
||||
facing : facing,
|
||||
floor : stops
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
if(group_cabins.length >= 2){
|
||||
|
||||
const stops = group_cabins.map(cabin => cabin.y);
|
||||
|
||||
group_cabins.forEach(cabin => {
|
||||
add(chunk, {
|
||||
group_id : group_id,
|
||||
x : cabin.x,
|
||||
y : cabin.y,
|
||||
z : cabin.z,
|
||||
facing : cabin.facing,
|
||||
floors_from : Math.min(...stops),
|
||||
floors_count : stops.length,
|
||||
stops : stops,
|
||||
floor : cabin.floor
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return chunk.elevators;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return ElevatorsBuilder;
|
||||
})();
|
||||
60
Public/ecma/Builders/FramesBuilder.ecma.js
Normal file
60
Public/ecma/Builders/FramesBuilder.ecma.js
Normal file
@ -0,0 +1,60 @@
|
||||
"use strict";
|
||||
|
||||
const FramesBuilder = (function(){
|
||||
|
||||
const FramesBuilder = function(hollow_bond){
|
||||
|
||||
const add = (chunk, inputs) => {
|
||||
|
||||
const id = FrameModel.get_id(inputs.x, inputs.y, inputs.z),
|
||||
frame = (
|
||||
chunk.frames[id] ||
|
||||
hollow_bond.map.frames[id] ||
|
||||
new FrameModel(hollow_bond, inputs)
|
||||
);
|
||||
|
||||
chunk.frames[id] = frame;
|
||||
// chunk.frames_by_position[id] = frame;
|
||||
|
||||
return frame;
|
||||
};
|
||||
|
||||
this.generate_seam = chunk => {
|
||||
|
||||
for(let py = chunk.yf; py < chunk.yt; py ++){
|
||||
|
||||
[[chunk.xf, "west"], [chunk.xt, "east"]].forEach(([wall_x, neighbor]) => {
|
||||
if(chunk.neighbors[neighbor])
|
||||
for(let cursor_z = chunk.zf, i = 0; cursor_z < chunk.zt; i ++)
|
||||
(
|
||||
cursor_z += Math.floor(hollow_bond.hash.seam_vertical_x(wall_x, py, chunk.zf, py + 1, chunk.zt, i) * hollow_bond.chunk_frame_maximum_gap) + 1
|
||||
) < chunk.zt && add(chunk, {
|
||||
x : wall_x,
|
||||
y : py,
|
||||
z : cursor_z,
|
||||
axis : "vertical"
|
||||
});
|
||||
});
|
||||
|
||||
[[chunk.zf, "north"], [chunk.zt, "south"]].forEach(([wall_z, neighbor]) => {
|
||||
if(chunk.neighbors[neighbor])
|
||||
for(let cursor_x = chunk.xf, i = 0; cursor_x < chunk.xt; i ++)
|
||||
(
|
||||
cursor_x += Math.floor(hollow_bond.hash.seam_vertical_z(chunk.xf, py, wall_z, chunk.xt, py + 1, i) * hollow_bond.chunk_frame_maximum_gap) + 1
|
||||
) < chunk.xt && add(chunk, {
|
||||
x : cursor_x,
|
||||
y : py,
|
||||
z : wall_z,
|
||||
axis : "horizontal"
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
return chunk.frames;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return FramesBuilder;
|
||||
})();
|
||||
141
Public/ecma/Builders/HolesBuilder.ecma.js
Normal file
141
Public/ecma/Builders/HolesBuilder.ecma.js
Normal file
@ -0,0 +1,141 @@
|
||||
"use strict";
|
||||
|
||||
const HolesBuilder = (function(){
|
||||
|
||||
const HolesBuilder = function(hollow_bond){
|
||||
|
||||
const add = (chunk, inputs) => {
|
||||
|
||||
const id = HoleModel.get_id(inputs.xf, inputs.yf, inputs.zf, inputs.xt, inputs.yt, inputs.zt),
|
||||
hole = (
|
||||
chunk.holes[id] ||
|
||||
hollow_bond.map.holes[id] ||
|
||||
new HoleModel(hollow_bond, inputs)
|
||||
);
|
||||
|
||||
chunk.holes[id] = hole;
|
||||
chunk.occupy(hole.xf, hole.yf, hole.zf, hole.xt, hole.yt, hole.zt, "hole", id);
|
||||
|
||||
hole.stairs.forEach(stair => {
|
||||
chunk.stairs[stair.id] = stair;
|
||||
});
|
||||
|
||||
return hole;
|
||||
};
|
||||
|
||||
this.generate_seam = chunk => {
|
||||
|
||||
[[chunk.yf, "down"], [chunk.yt, "up"]].forEach(([plane_y, neighbor]) => {
|
||||
if(!chunk.neighbors[neighbor])
|
||||
return;
|
||||
|
||||
const hole_attempts = Math.floor(hollow_bond.hash.seam_horizontal(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, 0) * hollow_bond.hole_seam_tries);
|
||||
|
||||
for(let i = 0; i < hole_attempts; i ++){
|
||||
|
||||
const width = Common.random_decay(
|
||||
hollow_bond.hash.seam_horizontal(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, i * 7 + 1),
|
||||
hollow_bond.hole_minimum_size,
|
||||
hollow_bond.hole_maximum_size,
|
||||
hollow_bond.hole_size_decay
|
||||
),
|
||||
depth = Common.random_decay(
|
||||
hollow_bond.hash.seam_horizontal(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, i * 7 + 2),
|
||||
hollow_bond.hole_minimum_size,
|
||||
hollow_bond.hole_maximum_size,
|
||||
hollow_bond.hole_size_decay
|
||||
),
|
||||
height = Common.random_decay(
|
||||
hollow_bond.hash.seam_horizontal(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, i * 7 + 3),
|
||||
hollow_bond.hole_minimum_height,
|
||||
hollow_bond.hole_maximum_height,
|
||||
hollow_bond.hole_height_decay
|
||||
),
|
||||
available_x = hollow_bond.chunk_size - width - hollow_bond.hole_margin * 2,
|
||||
available_z = hollow_bond.chunk_size - depth - hollow_bond.hole_margin * 2;
|
||||
|
||||
if(available_x <= 0 || available_z <= 0)
|
||||
continue;
|
||||
|
||||
const horizontal_xf = chunk.xf + hollow_bond.hole_margin + Math.floor(hollow_bond.hash.seam_horizontal(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, i * 7 + 5) * available_x),
|
||||
horizontal_xt = horizontal_xf + width,
|
||||
horizontal_zf = chunk.zf + hollow_bond.hole_margin + Math.floor(hollow_bond.hash.seam_horizontal(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, i * 7 + 6) * available_z),
|
||||
horizontal_zt = horizontal_zf + depth,
|
||||
penetration = Math.floor(hollow_bond.hash.seam_horizontal(chunk.xf, plane_y, chunk.zf, chunk.xt, chunk.zt, i * 7 + 4) * (height - 1)) + 1,
|
||||
horizontal_yf = plane_y - penetration,
|
||||
horizontal_yt = horizontal_yf + height;
|
||||
|
||||
chunk.is_occupied(horizontal_xf, horizontal_yf, horizontal_zf, horizontal_xt, horizontal_yt, horizontal_zt) || add(chunk, {
|
||||
xf : horizontal_xf,
|
||||
yf : horizontal_yf,
|
||||
zf : horizontal_zf,
|
||||
xt : horizontal_xt,
|
||||
yt : horizontal_yt,
|
||||
zt : horizontal_zt
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
return chunk.holes;
|
||||
};
|
||||
|
||||
this.generate_internal = chunk => {
|
||||
|
||||
for(let py = chunk.yf + 1; py < chunk.yt - hollow_bond.hole_minimum_height; py += hollow_bond.hole_floor_step){
|
||||
if(hollow_bond.hash.hole_internal(chunk.xf, py, chunk.zf, chunk.xt, py + 1, chunk.zt, 0) > hollow_bond.internal_hole_probability)
|
||||
continue;
|
||||
|
||||
const width = Common.random_decay(
|
||||
hollow_bond.hash.hole_internal(chunk.xf, py, chunk.zf, chunk.xt, py + 1, chunk.zt, 1),
|
||||
hollow_bond.hole_minimum_size,
|
||||
hollow_bond.hole_maximum_size,
|
||||
hollow_bond.hole_size_decay
|
||||
),
|
||||
depth = Common.random_decay(
|
||||
hollow_bond.hash.hole_internal(chunk.xf, py, chunk.zf, chunk.xt, py + 1, chunk.zt, 2),
|
||||
hollow_bond.hole_minimum_size,
|
||||
hollow_bond.hole_maximum_size,
|
||||
hollow_bond.hole_size_decay
|
||||
),
|
||||
raw_height = Common.random_decay(
|
||||
hollow_bond.hash.hole_internal(chunk.xf, py, chunk.zf, chunk.xt, py + 1, chunk.zt, 3),
|
||||
hollow_bond.hole_minimum_height,
|
||||
hollow_bond.hole_maximum_height,
|
||||
hollow_bond.hole_height_decay
|
||||
),
|
||||
available_x = hollow_bond.chunk_size - width - hollow_bond.hole_margin * 2,
|
||||
available_z = hollow_bond.chunk_size - depth - hollow_bond.hole_margin * 2;
|
||||
|
||||
if(available_x <= 0 || available_z <= 0)
|
||||
continue;
|
||||
|
||||
const internal_xf = chunk.xf + hollow_bond.hole_margin + Math.floor(hollow_bond.hash.hole_internal(chunk.xf, py, chunk.zf, chunk.xt, py + 1, chunk.zt, 4) * available_x),
|
||||
internal_xt = internal_xf + width,
|
||||
internal_zf = chunk.zf + hollow_bond.hole_margin + Math.floor(hollow_bond.hash.hole_internal(chunk.xf, py, chunk.zf, chunk.xt, py + 1, chunk.zt, 5) * available_z),
|
||||
internal_zt = internal_zf + depth,
|
||||
internal_yf = py,
|
||||
internal_yt = Math.min(chunk.yt - 1, internal_yf + raw_height);
|
||||
|
||||
if((internal_yt - internal_yf) < hollow_bond.hole_minimum_height)
|
||||
continue;
|
||||
|
||||
chunk.is_occupied(internal_xf, internal_yf, internal_zf, internal_xt, internal_yt, internal_zt) || add(chunk, {
|
||||
xf : internal_xf,
|
||||
yf : internal_yf,
|
||||
zf : internal_zf,
|
||||
xt : internal_xt,
|
||||
yt : internal_yt,
|
||||
zt : internal_zt
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
return chunk.holes;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return HolesBuilder;
|
||||
})();
|
||||
745
Public/ecma/Builders/RoomsBuilder.ecma.js
Normal file
745
Public/ecma/Builders/RoomsBuilder.ecma.js
Normal file
@ -0,0 +1,745 @@
|
||||
"use strict";
|
||||
|
||||
const RoomsBuilder = (function(){
|
||||
|
||||
const RoomsBuilder = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
|
||||
const get_hole_reserved_bounds_by_side = (hole, elevators, py) => {
|
||||
|
||||
const maximum_margin_x = Math.max(hollow_bond.room_minimum_distance_hole, (hollow_bond.room_maximum_size - Math.abs(hole.xt - hole.xf)) * 0.5),
|
||||
maximum_margin_z = Math.max(hollow_bond.room_minimum_distance_hole, (hollow_bond.room_maximum_size - Math.abs(hole.zt - hole.zf)) * 0.5),
|
||||
bounds = {
|
||||
xf : hole.xf - Common.random_decay(hollow_bond.hash.rooms(hole.xf, py, hole.zf, 1), hollow_bond.room_minimum_distance_hole, maximum_margin_x, hollow_bond.room_size_decay) | 0,
|
||||
xt : hole.xt + Common.random_decay(hollow_bond.hash.rooms(hole.xt, py, hole.zt, 2), hollow_bond.room_minimum_distance_hole, maximum_margin_x, hollow_bond.room_size_decay) | 0,
|
||||
zf : hole.zf - Common.random_decay(hollow_bond.hash.rooms(hole.xf, py, hole.zf, 3), hollow_bond.room_minimum_distance_hole, maximum_margin_z, hollow_bond.room_size_decay) | 0,
|
||||
zt : hole.zt + Common.random_decay(hollow_bond.hash.rooms(hole.xt, py, hole.zt, 4), hollow_bond.room_minimum_distance_hole, maximum_margin_z, hollow_bond.room_size_decay) | 0
|
||||
};
|
||||
|
||||
for(const elevator of elevators)
|
||||
if(
|
||||
bounds.xt > elevator.xf && bounds.xf < elevator.xt &&
|
||||
bounds.zt > elevator.zf && bounds.zf < elevator.zt
|
||||
){
|
||||
|
||||
const distance = {
|
||||
xf : elevator.xt - bounds.xf,
|
||||
xt : bounds.xt - elevator.xf,
|
||||
zf : elevator.zt - bounds.zf,
|
||||
zt : bounds.zt - elevator.zf
|
||||
};
|
||||
|
||||
switch(Math.min(distance.xf, distance.xt, distance.zf, distance.zt)){
|
||||
case distance.xf:
|
||||
bounds.xf = elevator.xt;
|
||||
break;
|
||||
case distance.xt:
|
||||
bounds.xt = elevator.xf;
|
||||
break;
|
||||
case distance.zf:
|
||||
bounds.zf = elevator.zt;
|
||||
break;
|
||||
case distance.zt:
|
||||
bounds.zt = elevator.zf;
|
||||
break;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return bounds;
|
||||
};
|
||||
|
||||
const cut_z = (cx, rectangle) => [
|
||||
{xf : rectangle.xf, xt : cx, zf : rectangle.zf, zt : rectangle.zt, done : false},
|
||||
{xf : cx, xt : rectangle.xt, zf : rectangle.zf, zt : rectangle.zt, done : false}
|
||||
];
|
||||
|
||||
const cut_x = (cz, rectangle) => [
|
||||
{xf : rectangle.xf, xt : rectangle.xt, zf : rectangle.zf, zt : cz, done : false},
|
||||
{xf : rectangle.xf, xt : rectangle.xt, zf : cz, zt : rectangle.zt, done : false}
|
||||
];
|
||||
|
||||
const obstacules_cut_z = (cx, obstacules, rectangle) => obstacules.some(obstacule => (
|
||||
cx > obstacule.xf && cx < obstacule.xt &&
|
||||
Math.max(rectangle.zf, obstacule.zf) < Math.min(rectangle.zt, obstacule.zt)
|
||||
)) ? [false, null] : [true, cut_z(cx, rectangle)];
|
||||
|
||||
const obstacules_cut_x = (cz, obstacules, rectangle) => obstacules.some(obstacule => (
|
||||
cz > obstacule.zf && cz < obstacule.zt &&
|
||||
Math.max(rectangle.xf, obstacule.xf) < Math.min(rectangle.xt, obstacule.xt)
|
||||
)) ? [false, null] : [true, cut_x(cz, rectangle)];
|
||||
|
||||
const subdivide_rooms = (rectangles, py) => {
|
||||
|
||||
let has_changes = false;
|
||||
|
||||
for(const rectangle of rectangles){
|
||||
if(!rectangle.free)
|
||||
rectangle.done = true;
|
||||
else if(rectangle.done === undefined)
|
||||
rectangle.done = !rectangle.free;
|
||||
};
|
||||
|
||||
do{
|
||||
|
||||
const new_rooms = [];
|
||||
|
||||
has_changes = false;
|
||||
|
||||
for(const room of rectangles){
|
||||
|
||||
if(room.done || !room.free)
|
||||
continue;
|
||||
|
||||
const width = Math.abs(room.xt - room.xf),
|
||||
depth = Math.abs(room.zt - room.zf);
|
||||
|
||||
const can_split_x = width >= hollow_bond.room_minimum_size * 2,
|
||||
can_split_z = depth >= hollow_bond.room_minimum_size * 2;
|
||||
|
||||
if(!can_split_x && !can_split_z){
|
||||
room.done = true;
|
||||
continue;
|
||||
};
|
||||
|
||||
const x_oversized = width > hollow_bond.room_maximum_size,
|
||||
z_oversized = depth > hollow_bond.room_maximum_size,
|
||||
is_oversized = x_oversized || z_oversized,
|
||||
// 1. Hash con firma 2D completa (evita cuadrículas alineadas entre vecinos)
|
||||
seed_x = (room.xf * 73856093) ^ (room.xt * 19349663),
|
||||
seed_z = (room.zf * 83492791) ^ (room.zt * 50624027);
|
||||
let should_split = is_oversized;
|
||||
|
||||
if(!should_split)
|
||||
should_split = hollow_bond.hash.rooms(seed_x, py, seed_z, 10) < (Math.max(width, depth) - hollow_bond.room_minimum_size * 2) / Math.max(1, hollow_bond.room_maximum_size - hollow_bond.room_minimum_size * 2);
|
||||
|
||||
if (!should_split) {
|
||||
room.done = true;
|
||||
continue;
|
||||
};
|
||||
|
||||
// 2. Selección de eje: romper la relación de aspecto para forzar habitaciones cuadradas
|
||||
let split_axis = null;
|
||||
|
||||
if(can_split_x && can_split_z){
|
||||
// Si uno supera el máximo y el otro no, cortar obligatoriamente el sobredimensionado
|
||||
if(x_oversized && !z_oversized)
|
||||
split_axis = 'x';
|
||||
else if(z_oversized && !x_oversized)
|
||||
split_axis = 'z';
|
||||
else
|
||||
// Ponderación cuadrática por aspecto para evitar tiras/pasillos
|
||||
split_axis = hollow_bond.hash.rooms(seed_x, py, seed_z, 11) < width * width / (width * width + depth * depth) ? 'x' : 'z';
|
||||
}else
|
||||
split_axis = can_split_x ? 'x' : 'z';
|
||||
|
||||
// 3. Punto de corte: oscilar cerca del centro en lugar de pelar los bordes
|
||||
if(split_axis === 'x'){
|
||||
|
||||
const split_x = (room.xf + Math.max(hollow_bond.room_minimum_size, Math.min(width - hollow_bond.room_minimum_size, width * .5 + Common.random_decay(hollow_bond.hash.rooms(seed_x, py, seed_z, 12), 0, (width - hollow_bond.room_minimum_size * 2) * .5, hollow_bond.room_size_decay) * (hollow_bond.hash.rooms(seed_x, py, seed_z, 14) < .5 ? -1 : 1)))) | 0,
|
||||
old_xt = room.xt;
|
||||
|
||||
room.xt = split_x;
|
||||
room.done = false;
|
||||
|
||||
new_rooms.push({
|
||||
xf : split_x,
|
||||
xt : old_xt,
|
||||
zf : room.zf,
|
||||
zt : room.zt,
|
||||
free : true,
|
||||
done : false
|
||||
});
|
||||
|
||||
}else{
|
||||
|
||||
const split_z = (room.zf + Math.max(hollow_bond.room_minimum_size, Math.min(depth - hollow_bond.room_minimum_size, depth * .5 + Common.random_decay(hollow_bond.hash.rooms(seed_z, py, seed_x, 13), 0, depth - hollow_bond.room_minimum_size * 2 * .5, hollow_bond.room_size_decay) * (hollow_bond.hash.rooms(seed_z, py, seed_x, 15) < .5 ? -1 : 1)))) | 0,
|
||||
old_zt = room.zt;
|
||||
|
||||
room.zt = split_z;
|
||||
room.done = false;
|
||||
|
||||
new_rooms.push({
|
||||
xf : room.xf,
|
||||
xt : room.xt,
|
||||
zf : split_z,
|
||||
zt : old_zt,
|
||||
free : true,
|
||||
done : false
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
has_changes = true;
|
||||
|
||||
};
|
||||
|
||||
new_rooms.length > 0 && rectangles.push(...new_rooms);
|
||||
|
||||
}while(has_changes);
|
||||
|
||||
return rectangles;
|
||||
};
|
||||
|
||||
const divide_plane = (chunk, py) => {
|
||||
|
||||
const elevators = Object.values(chunk.elevators).filter(elevator => elevator.y == py).map(elevator => ({
|
||||
xf : elevator.x,
|
||||
xt : elevator.x + elevator.width,
|
||||
zf : elevator.z,
|
||||
zt : elevator.z + elevator.depth
|
||||
})),
|
||||
holes_y = Object.values(chunk.holes).filter(hole => py >= hole.yf && py < hole.yt),
|
||||
holes = holes_y.map(hole => get_hole_reserved_bounds_by_side(hole, elevators, py)),
|
||||
obstacules = elevators.concat(holes),
|
||||
queue = [{ xf : chunk.xf, xt : chunk.xt, zf : chunk.zf, zt : chunk.zt }],
|
||||
obstacules_cut = { x : obstacules_cut_x, z : obstacules_cut_z },
|
||||
// 3. Ajuste de habitaciones sobredimensionadas que contienen huecos
|
||||
new_rectangles = [];
|
||||
let rectangles = [];
|
||||
|
||||
// 1. Partición de espacios con cola estricta
|
||||
while(queue.length > 0){
|
||||
|
||||
const rectangle = queue.pop(),
|
||||
local_obstacules = obstacules.filter(obstacule =>
|
||||
obstacule.xt > rectangle.xf && obstacule.xf < rectangle.xt &&
|
||||
obstacule.zt > rectangle.zf && obstacule.zf < rectangle.zt
|
||||
);
|
||||
|
||||
if(local_obstacules.length === 0){
|
||||
rectangles.push({...rectangle, free : true});
|
||||
continue;
|
||||
};
|
||||
|
||||
if(
|
||||
local_obstacules.length === 1 &&
|
||||
rectangle.xf === local_obstacules[0].xf &&
|
||||
rectangle.xt === local_obstacules[0].xt &&
|
||||
rectangle.zf === local_obstacules[0].zf &&
|
||||
rectangle.zt === local_obstacules[0].zt
|
||||
){
|
||||
rectangles.push({...rectangle, free : false});
|
||||
continue;
|
||||
};
|
||||
|
||||
let split_found = false;
|
||||
|
||||
for(const obstacule of local_obstacules){
|
||||
for (const [axis, value] of [].concat(
|
||||
[["z", obstacule.xf, 0], ["z", obstacule.xt, 1]].filter(([_, x]) => x > rectangle.xf && x < rectangle.xt),
|
||||
[["x", obstacule.zf, 2], ["x", obstacule.zt, 3]].filter(([_, z]) => z > rectangle.zf && z < rectangle.zt)
|
||||
).sort((a, b) => hollow_bond.hash.room_cut_axis(py, a, b))){
|
||||
|
||||
const [ok, subrectangles] = obstacules_cut[axis](value, local_obstacules, rectangle);
|
||||
|
||||
if(ok){
|
||||
queue.push(...subrectangles);
|
||||
split_found = true;
|
||||
break;
|
||||
};
|
||||
|
||||
};
|
||||
if(split_found)
|
||||
break;
|
||||
};
|
||||
|
||||
split_found || rectangles.push({...rectangle, free : !obstacules.some(obstacule =>
|
||||
rectangle.xf >= obstacule.xf && rectangle.xt <= obstacule.xt &&
|
||||
rectangle.zf >= obstacule.zf && rectangle.zt <= obstacule.zt
|
||||
)});
|
||||
|
||||
};
|
||||
|
||||
// 2. Descartar los rectángulos que corresponden exactamente a los ascensores
|
||||
rectangles = rectangles.filter(rectangle => !(!rectangle.free && elevators.some(elevator => (
|
||||
rectangle.xf === elevator.xf && rectangle.xt === elevator.xt &&
|
||||
rectangle.zf === elevator.zf && rectangle.zt === elevator.zt
|
||||
))));
|
||||
|
||||
for(let i = 0; i < rectangles.length; i ++){
|
||||
|
||||
// Obtener los agujeros reales dentro de este rectángulo
|
||||
const contained_holes = holes_y.filter(hole => (
|
||||
rectangles[i].xf <= hole.xf && rectangles[i].xt >= hole.xt &&
|
||||
rectangles[i].zf <= hole.zf && rectangles[i].zt >= hole.zt
|
||||
));
|
||||
|
||||
if(contained_holes.length === 0)
|
||||
continue;
|
||||
|
||||
let width = Math.abs(rectangles[i].xt - rectangles[i].xf),
|
||||
depth = Math.abs(rectangles[i].zt - rectangles[i].zf);
|
||||
|
||||
if(width > hollow_bond.room_maximum_size || depth > hollow_bond.room_maximum_size){
|
||||
|
||||
// Bounding box protector envolvente de todos los agujeros contenidos
|
||||
const minimum = {
|
||||
xf : Math.min(...contained_holes.map(hole => hole.xf)) - hollow_bond.room_minimum_distance_hole,
|
||||
xt : Math.max(...contained_holes.map(hole => hole.xt)) + hollow_bond.room_minimum_distance_hole,
|
||||
zf : Math.min(...contained_holes.map(hole => hole.zf)) - hollow_bond.room_minimum_distance_hole,
|
||||
zt : Math.max(...contained_holes.map(hole => hole.zt)) + hollow_bond.room_minimum_distance_hole
|
||||
};
|
||||
|
||||
// Recortes en X
|
||||
if(width > hollow_bond.room_maximum_size){
|
||||
if(rectangles[i].xf < minimum.xf){
|
||||
|
||||
const subrectangles = cut_z(minimum.xf, rectangles[i]);
|
||||
|
||||
width -= Math.abs(minimum.xf - rectangles[i].xf);
|
||||
rectangles[i] = subrectangles[1];
|
||||
new_rectangles.push(subrectangles[0]);
|
||||
|
||||
};
|
||||
if(width > hollow_bond.room_maximum_size && rectangles[i].xt > minimum.xt){
|
||||
|
||||
const subrectangles = cut_z(minimum.xt, rectangles[i]);
|
||||
|
||||
rectangles[i] = subrectangles[0];
|
||||
new_rectangles.push(subrectangles[1]);
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
// Recortes en Z
|
||||
if(depth > hollow_bond.room_maximum_size){
|
||||
if(rectangles[i].zf < minimum.zf){
|
||||
|
||||
const subrectangles = cut_x(minimum.zf, rectangles[i]);
|
||||
|
||||
depth -= Math.abs(minimum.zf - rectangles[i].zf);
|
||||
rectangles[i] = subrectangles[1];
|
||||
new_rectangles.push(subrectangles[0]);
|
||||
|
||||
};
|
||||
if(depth > hollow_bond.room_maximum_size && rectangles[i].zt > minimum.zt){
|
||||
|
||||
const subrectangles = cut_x(minimum.zt, rectangles[i]);
|
||||
|
||||
rectangles[i] = subrectangles[0];
|
||||
new_rectangles.push(subrectangles[1]);
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
rectangles.push(...new_rectangles);
|
||||
|
||||
// Blindar las habitaciones que contienen o tocan fosos para que no sean cortadas
|
||||
for(const rect of rectangles){
|
||||
|
||||
const touches_hole = holes_y.some(hole => (
|
||||
rect.xt > hole.xf && rect.xf < hole.xt &&
|
||||
rect.zt > hole.zf && rect.zf < hole.zt
|
||||
));
|
||||
|
||||
if(touches_hole){
|
||||
rect.done = true;
|
||||
rect.free = false;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return subdivide_rooms(rectangles, py);
|
||||
};
|
||||
|
||||
const get_bounds = (x, z, axis) => {
|
||||
|
||||
const margin = hollow_bond.frame_padding + hollow_bond.room_padding
|
||||
|
||||
return {
|
||||
xf : axis === "horizontal" ? x : x - margin,
|
||||
xt : axis === "horizontal" ? x + hollow_bond.frame_width : x + margin,
|
||||
zf : axis === "horizontal" ? z - margin : z,
|
||||
zt : axis === "horizontal" ? z + margin : z + hollow_bond.frame_width
|
||||
};
|
||||
};
|
||||
|
||||
const collides = (b1, b2) => (
|
||||
b1.xt > b2.xf && b1.xf < b2.xt &&
|
||||
b1.zt > b2.zf && b1.zf < b2.zt
|
||||
);
|
||||
|
||||
/**
|
||||
* Genera los marcos interiores entre habitaciones en un piso determinado.
|
||||
* @param {ChunkModel} chunk - Instancia del Chunk actual.
|
||||
* @param {Array<RoomModel>} rooms - Habitaciones del piso actual.
|
||||
* @param {number} py - Índice del piso.
|
||||
*/
|
||||
const generate_frames = (chunk, rooms, py) => {
|
||||
|
||||
chunk.frames = chunk.frames || {};
|
||||
|
||||
const margin = hollow_bond.frame_padding + hollow_bond.room_padding,
|
||||
chunk_xf = chunk.xf,
|
||||
chunk_xt = chunk.xt,
|
||||
chunk_zf = chunk.zf,
|
||||
chunk_zt = chunk.zt,
|
||||
// Estructura para registrar los AABB de los marcos ya aceptados en este piso
|
||||
accepted_bounds = [],
|
||||
// Evaluamos cada par de habitaciones del piso una sola vez
|
||||
total_rooms = rooms.length;
|
||||
|
||||
for(let i = 0; i < total_rooms; i ++){
|
||||
|
||||
const room_a = rooms[i];
|
||||
|
||||
for(const room_b of rooms){
|
||||
|
||||
let candidate = null;
|
||||
|
||||
// 1. Comprobar pared vertical compartida (normal en X, discurre a lo largo de Z)
|
||||
if(room_a.x + room_a.width === room_b.x || room_b.x + room_b.width === room_a.x){
|
||||
|
||||
const wall_x = (room_a.x + room_a.width === room_b.x) ? room_b.x : room_a.x;
|
||||
|
||||
// Descartar si coincide con el límite exterior del Chunk
|
||||
if(wall_x > chunk_xf && wall_x < chunk_xt){
|
||||
|
||||
// Dejamos margen para no pegar el marco a las esquinas ni al borde del Chunk
|
||||
const minimum_z = Math.max(Math.max(room_a.z, room_b.z) + margin, chunk_zf + margin),
|
||||
maximum_z = Math.min(Math.min(room_a.z + room_a.depth, room_b.z + room_b.depth) - margin, chunk_zt - margin),
|
||||
span = maximum_z - minimum_z;
|
||||
|
||||
if(span >= hollow_bond.frame_width && hollow_bond.hash.rooms(wall_x, py, (minimum_z + maximum_z) | 0, 20) < hollow_bond.frame_wall_probability)
|
||||
candidate = {
|
||||
axis : "vertical",
|
||||
x : wall_x,
|
||||
z : minimum_z + (Common.random_decay(hollow_bond.hash.rooms(wall_x, py, (minimum_z + maximum_z) | 0, 21), 0, span - hollow_bond.frame_width, hollow_bond.frame_wall_decay) | 0),
|
||||
room_a : room_a,
|
||||
room_b : room_b
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// 2. Comprobar pared horizontal compartida (normal en Z, discurre a lo largo de X)
|
||||
if(!candidate && (room_a.z + room_a.depth === room_b.z || room_b.z + room_b.depth === room_a.z)){
|
||||
|
||||
const wall_z = room_a.z + room_a.depth === room_b.z ? room_b.z : room_a.z;
|
||||
|
||||
// Descartar si coincide con el límite exterior del Chunk
|
||||
if(wall_z > chunk_zf && wall_z < chunk_zt){
|
||||
|
||||
const minimum_x = Math.max(Math.max(room_a.x, room_b.x) + margin, chunk_xf + margin),
|
||||
maximum_x = Math.min(Math.min(room_a.x + room_a.width, room_b.x + room_b.width) - margin, chunk_xt - margin),
|
||||
span = maximum_x - minimum_x;
|
||||
|
||||
if(span >= hollow_bond.frame_width && hollow_bond.hash.rooms((minimum_x + maximum_x) | 0, py, wall_z, 22) < hollow_bond.frame_wall_probability)
|
||||
candidate = {
|
||||
axis : "horizontal",
|
||||
x : minimum_x + (Common.random_decay(hollow_bond.hash.rooms((minimum_x + maximum_x) | 0, py, wall_z, 23), 0, span - hollow_bond.frame_width, hollow_bond.frame_wall_decay) | 0),
|
||||
z : wall_z,
|
||||
room_a : room_a,
|
||||
room_b : room_b
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// 3. Si hay candidato, verificar que no colisiona con ningún marco existente
|
||||
if(candidate){
|
||||
|
||||
const room_b_can = get_bounds(candidate.x, candidate.z, candidate.axis);
|
||||
|
||||
if(!accepted_bounds.some(bound => collides(bound, room_b_can))){
|
||||
|
||||
const frame = new FrameModel(hollow_bond, {
|
||||
x: candidate.x,
|
||||
y: py,
|
||||
z: candidate.z,
|
||||
axis: candidate.axis,
|
||||
rooms: [candidate.room_a.id, candidate.room_b.id]
|
||||
});
|
||||
|
||||
accepted_bounds.push(room_b_can);
|
||||
|
||||
// Vinculación bidireccional
|
||||
candidate.room_a.frames.push(frame.id);
|
||||
candidate.room_b.frames.push(frame.id);
|
||||
|
||||
// Registro en el Chunk y en el mapa global
|
||||
chunk.frames[frame.id] = frame;
|
||||
|
||||
if(hollow_bond.map && hollow_bond.map.frames)
|
||||
hollow_bond.map.frames[frame.id] = frame;
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Conecta los marcos perimetrales del Chunk con las habitaciones correspondientes.
|
||||
* @param {ChunkModel} chunk - Instancia del Chunk actual.
|
||||
*/
|
||||
const connect_chunk_frames = chunk => {
|
||||
|
||||
const eps = .001,
|
||||
// Obtenemos todos los marcos del Chunk que tengan conexiones libres ([null, null] o [id, null])
|
||||
chunk_frames = Object.values(chunk.frames).filter(frame => frame.rooms && frame.rooms.includes(null));
|
||||
|
||||
if(chunk_frames.length === 0)
|
||||
return;
|
||||
|
||||
// Buscamos contra todas las habitaciones disponibles (las del mapa global para unir con vecinos, o las del chunk)
|
||||
const available_rooms = Object.values(chunk.rooms);
|
||||
|
||||
for(const frame of chunk_frames)
|
||||
for(const room of available_rooms)
|
||||
// 1. Comprobar que coincidan en el piso
|
||||
if(Math.round(room.y / (room.height || hollow_bond.room_height)) === frame.y && (frame.axis === "horizontal" ? (
|
||||
(Math.abs(room.z + room.depth - frame.z) < eps || Math.abs(room.z - frame.z) < eps) &&
|
||||
frame.x >= room.x - eps && frame.x + frame.width <= room.x + room.width + eps
|
||||
) : (
|
||||
(Math.abs(room.x + room.width - frame.x) < eps || Math.abs(room.x - frame.x) < eps) &&
|
||||
frame.z >= room.z - eps && frame.z + frame.width <= room.z + room.depth + eps
|
||||
))){
|
||||
|
||||
// Vinculación bidireccional
|
||||
frame.connect_room(room.id);
|
||||
|
||||
room.frames.includes(frame.id) || room.frames.push(frame.id);
|
||||
|
||||
// Si el marco ya conectó sus 2 habitaciones, no hace falta evaluar más para este marco
|
||||
if(!frame.rooms.includes(null))
|
||||
break;
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
const get_frame = (chunk, id) => chunk.frames[id] || hollow_bond.map.frames[id];
|
||||
|
||||
// Criterio para identificar si un marco da al exterior del Chunk
|
||||
const is_chunk_frame = (chunk, frame) => {
|
||||
if(!frame)
|
||||
return false;
|
||||
if(
|
||||
(chunk.chunk_frames && Object.values(chunk.chunk_frames).some(f => f.id === frame.id)) ||
|
||||
frame.is_chunk_frame ||
|
||||
(frame.rooms && frame.rooms.includes(null))
|
||||
)
|
||||
return true;
|
||||
|
||||
// Comprobación geométrica: el marco reposa sobre los bordes del Chunk
|
||||
const eps = .5;
|
||||
|
||||
return (
|
||||
Math.abs(frame.x - chunk.xf) < eps || Math.abs(frame.x - chunk.xt) < eps ||
|
||||
Math.abs(frame.z - chunk.zf) < eps || Math.abs(frame.z - chunk.zt) < eps
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Elimina habitaciones aisladas e islas cerradas dentro del Chunk.
|
||||
* @param {ChunkModel} chunk - Instancia del Chunk actual.
|
||||
*/
|
||||
const cleanup_unconnected_rooms = chunk => {
|
||||
|
||||
// 1. Identificar las habitaciones "raíz" (tienen salida directa garantizada)
|
||||
const reachable = new Set(),
|
||||
queue = [];
|
||||
|
||||
for(const room_id in chunk.rooms)
|
||||
if(
|
||||
(chunk.rooms[room_id].elevators && chunk.rooms[room_id].elevators.length > 0) ||
|
||||
(chunk.rooms[room_id].holes && chunk.rooms[room_id].holes.length > 0) ||
|
||||
(chunk.rooms[room_id].frames && chunk.rooms[room_id].frames.some(frame_id => is_chunk_frame(chunk, get_frame(chunk, frame_id))))
|
||||
){
|
||||
reachable.add(chunk.rooms[room_id].id);
|
||||
queue.push(chunk.rooms[room_id]);
|
||||
};
|
||||
|
||||
// 2. Propagar la alcanzabilidad mediante BFS a través de los marcos transitables
|
||||
while(queue.length > 0){
|
||||
|
||||
const current = queue.shift();
|
||||
|
||||
if(!current.frames)
|
||||
continue;
|
||||
|
||||
for(const frame_id of current.frames){
|
||||
|
||||
const frame = get_frame(chunk, frame_id);
|
||||
|
||||
if(!frame || !frame.rooms)
|
||||
continue;
|
||||
|
||||
for(const neighbor_id of frame.rooms)
|
||||
// Si el vecino pertenece a este Chunk y aún no fue alcanzado
|
||||
if(neighbor_id && chunk.rooms[neighbor_id] && !reachable.has(neighbor_id)){
|
||||
reachable.add(neighbor_id);
|
||||
queue.push(chunk.rooms[neighbor_id]);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// 3. Purgar las habitaciones inalcanzables y sus marcos huérfanos
|
||||
const frames_to_delete = new Set();
|
||||
|
||||
for(const room_id in chunk.rooms)
|
||||
if(!reachable.has(room_id)){
|
||||
|
||||
// Marcar sus marcos para eliminarlos
|
||||
if (chunk.rooms[room_id].frames)
|
||||
for (const frame_id of chunk.rooms[room_id].frames)
|
||||
frames_to_delete.add(frame_id);
|
||||
|
||||
// Eliminar habitación del Chunk y del mapa global
|
||||
delete chunk.rooms[room_id];
|
||||
delete hollow_bond.map.rooms[room_id];
|
||||
|
||||
};
|
||||
|
||||
// 4. Limpiar los marcos que pertenecían a las habitaciones eliminadas
|
||||
for(const frame_id of frames_to_delete){
|
||||
delete chunk.frames[frame_id];
|
||||
delete hollow_bond.map.frames[frame_id];
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
const generate_floor = (chunk, py) => {
|
||||
|
||||
const eps = .05;
|
||||
|
||||
|
||||
// Delegación directa a tu método en HollowBond
|
||||
const rectangles = divide_plane(chunk, py),
|
||||
rooms = [],
|
||||
floor_elevators = Object.values(chunk.elevators).filter(elevator => elevator.y == py);
|
||||
|
||||
if(!Array.isArray(rectangles))
|
||||
return;
|
||||
|
||||
for(let i = 0; i < rectangles.length; i ++){
|
||||
|
||||
const width = rectangles[i].xt - rectangles[i].xf,
|
||||
depth = rectangles[i].zt - rectangles[i].zf;
|
||||
|
||||
if(width <= 0 || depth <= 0)
|
||||
continue;
|
||||
|
||||
// Asociar huecos que toquen o queden dentro de este rectángulo en este piso
|
||||
const associated_holes = [],
|
||||
// Asociar ascensores presentes en la habitación en este piso
|
||||
associated_elevators = [];
|
||||
let room;
|
||||
|
||||
for(const elevator of floor_elevators){
|
||||
|
||||
let is_connected = false;
|
||||
|
||||
switch(elevator.facing){
|
||||
case "north":
|
||||
is_connected = Math.abs(rectangles[i].zt - elevator.z) < eps && (Math.max(rectangles[i].xf, elevator.x) < Math.min(rectangles[i].xt, elevator.x + elevator.width));
|
||||
break;
|
||||
case "south":
|
||||
is_connected = Math.abs(rectangles[i].zf - (elevator.z + elevator.depth)) < eps && (Math.max(rectangles[i].xf, elevator.x) < Math.min(rectangles[i].xt, elevator.x + elevator.width));
|
||||
break;
|
||||
case "west":
|
||||
is_connected = Math.abs(rectangles[i].xt - elevator.x) < eps && (Math.max(rectangles[i].zf, elevator.z) < Math.min(rectangles[i].zt, elevator.z + elevator.depth));
|
||||
break;
|
||||
case "east":
|
||||
is_connected = Math.abs(rectangles[i].xf - (elevator.x + elevator.width)) < eps && (Math.max(rectangles[i].zf, elevator.z) < Math.min(rectangles[i].zt, elevator.z + elevator.depth));
|
||||
break;
|
||||
};
|
||||
|
||||
is_connected && associated_elevators.push(elevator.id);
|
||||
|
||||
};
|
||||
|
||||
for(const hole_id in chunk.holes)
|
||||
!(py < chunk.holes[hole_id].yf || py >= chunk.holes[hole_id].yt) && (
|
||||
rectangles[i].xt > chunk.holes[hole_id].xf && rectangles[i].xf < chunk.holes[hole_id].xt &&
|
||||
rectangles[i].zt > chunk.holes[hole_id].zf && rectangles[i].zf < chunk.holes[hole_id].zt
|
||||
) && associated_holes.push(chunk.holes[hole_id].id || hole_id);
|
||||
|
||||
rooms.push(room = new RoomModel(hollow_bond, {
|
||||
x : rectangles[i].xf,
|
||||
y : py * hollow_bond.room_height,
|
||||
z : rectangles[i].zf,
|
||||
width : width,
|
||||
height : hollow_bond.room_height,
|
||||
depth : depth,
|
||||
padding : hollow_bond.room_padding,
|
||||
holes : associated_holes,
|
||||
elevators : associated_elevators,
|
||||
frames : []
|
||||
}));
|
||||
// Registro en el Chunk y en el consolidado global de MapManager
|
||||
chunk.rooms[room.id] = room;
|
||||
|
||||
for(const id of associated_holes)
|
||||
chunk.holes[id].rooms.push(room.id);
|
||||
for(const id of associated_elevators)
|
||||
chunk.elevators[id].room = room.id;
|
||||
|
||||
if(hollow_bond.map && hollow_bond.map.rooms)
|
||||
hollow_bond.map.rooms[room.id] = room.id;
|
||||
|
||||
};
|
||||
|
||||
generate_frames(chunk, rooms, py);
|
||||
|
||||
};
|
||||
|
||||
this.generate_asyncronous = (chunk, callback) => new Promise(resolve => {
|
||||
|
||||
let py = chunk.yf;
|
||||
const secure_time = 5,
|
||||
tick = () => {
|
||||
|
||||
const start_time = performance.now();
|
||||
|
||||
while(py < chunk.yt && performance.now() - start_time < secure_time)
|
||||
generate_floor(chunk, py ++);
|
||||
|
||||
if(py < chunk.yt)
|
||||
requestAnimationFrame(tick);
|
||||
else{
|
||||
connect_chunk_frames(chunk);
|
||||
cleanup_unconnected_rooms(chunk);
|
||||
callback();
|
||||
resolve();
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
requestAnimationFrame(tick);
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Genera las instancias de RoomModel a partir de los rectángulos devueltos por divide_plane.
|
||||
* @param {ChunkModel} chunk - Instancia del Chunk padre.
|
||||
*/
|
||||
this.generate = chunk => {
|
||||
|
||||
for(let py = chunk.yf; py < chunk.yt; py ++)
|
||||
generate_floor(chunk, py);
|
||||
|
||||
connect_chunk_frames(chunk);
|
||||
cleanup_unconnected_rooms(chunk);
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return RoomsBuilder;
|
||||
})();
|
||||
91
Public/ecma/Builders/StairsBuilder.ecma.js
Normal file
91
Public/ecma/Builders/StairsBuilder.ecma.js
Normal file
@ -0,0 +1,91 @@
|
||||
"use strict";
|
||||
|
||||
const StairsBuilder = (function(){
|
||||
|
||||
const StairsBuilder = function(hollow_bond){
|
||||
|
||||
const is_volume_free = (occupied, xf, yf, zf, xt, yt, zt) => {
|
||||
for(let x = xf; x < xt; x ++)
|
||||
for(let y = yf; y < yt; y ++)
|
||||
for(let z = zf; z < zt; z ++)
|
||||
if(occupied.has(`${x}_${y}_${z}`))
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const reserve_volume = (occupied, xf, yf, zf, xt, yt, zt) => {
|
||||
for(let x = xf; x < xt; x ++)
|
||||
for(let y = yf; y < yt; y ++)
|
||||
for(let z = zf; z < zt; z ++)
|
||||
occupied.add(`${x}_${y}_${z}`);
|
||||
};
|
||||
|
||||
this.generate = hole => {
|
||||
if(hole.height < hollow_bond.stairs_height)
|
||||
return hole.stairs;
|
||||
|
||||
const occupied = new Set();
|
||||
|
||||
for(let py = hole.yf; py <= hole.yt - hollow_bond.stairs_height; py++){
|
||||
|
||||
if(hollow_bond.hash.stairs(hole.xf, py, hole.zf, hole.xt, py + hollow_bond.stairs_height, hole.zt, 0) > hollow_bond.stairs_probability)
|
||||
continue;
|
||||
|
||||
const wall_i = Math.floor(hollow_bond.hash.stairs(hole.xf, py, hole.zf, hole.xt, py + hollow_bond.stairs_height, hole.zt, 1) * 4);
|
||||
let stairs_xf, stairs_zf, stairs_xt, stairs_zt, wall_name;
|
||||
|
||||
if(wall_i === 0 && hole.width >= hollow_bond.stairs_length){ // Pared Norte (Z = zf)
|
||||
wall_name = "north";
|
||||
stairs_xf = hole.xf + Math.floor(hollow_bond.hash.stairs(hole.xf, py, hole.zf, hole.xt, py + hollow_bond.stairs_height, hole.zt, 2) * (hole.width - hollow_bond.stairs_length + 1));
|
||||
stairs_xt = stairs_xf + hollow_bond.stairs_length;
|
||||
stairs_zf = hole.zf;
|
||||
stairs_zt = stairs_zf + 1;
|
||||
}else if(wall_i === 1 && hole.width >= hollow_bond.stairs_length){ // Pared Sur (Z = zt - 1)
|
||||
wall_name = "south";
|
||||
stairs_xf = hole.xf + Math.floor(hollow_bond.hash.stairs(hole.xf, py, hole.zf, hole.xt, py + hollow_bond.stairs_height, hole.zt, 2) * (hole.width - hollow_bond.stairs_length + 1));
|
||||
stairs_xt = stairs_xf + hollow_bond.stairs_length;
|
||||
stairs_zt = hole.zt;
|
||||
stairs_zf = stairs_zt - 1;
|
||||
}else if(wall_i === 2 && hole.depth >= hollow_bond.stairs_length){ // Pared Oeste (X = xf)
|
||||
wall_name = "west";
|
||||
stairs_zf = hole.zf + Math.floor(hollow_bond.hash.stairs(hole.xf, py, hole.zf, hole.xt, py + hollow_bond.stairs_height, hole.zt, 2) * (hole.depth - hollow_bond.stairs_length + 1));
|
||||
stairs_zt = stairs_zf + hollow_bond.stairs_length;
|
||||
stairs_xf = hole.xf;
|
||||
stairs_xt = stairs_xf + 1;
|
||||
}else if(wall_i === 3 && hole.depth >= hollow_bond.stairs_length){ // Pared Este (X = xt - 1)
|
||||
wall_name = "east";
|
||||
stairs_zf = hole.zf + Math.floor(hollow_bond.hash.stairs(hole.xf, py, hole.zf, hole.xt, py + hollow_bond.stairs_height, hole.zt, 2) * (hole.depth - hollow_bond.stairs_length + 1));
|
||||
stairs_zt = stairs_zf + hollow_bond.stairs_length;
|
||||
stairs_xt = hole.xt;
|
||||
stairs_xf = stairs_xt - 1;
|
||||
}else
|
||||
continue;
|
||||
|
||||
const clearance_yt = Math.min(hole.yt, py + hollow_bond.stairs_height + 1);
|
||||
|
||||
if(!is_volume_free(occupied, stairs_xf, py, stairs_zf, stairs_xt, clearance_yt, stairs_zt))
|
||||
continue; // Hay conflicto vertical con otra escalera o descansillo
|
||||
|
||||
reserve_volume(occupied, stairs_xf, py, stairs_zf, stairs_xt, clearance_yt, stairs_zt);
|
||||
|
||||
hole.stairs.push(new StairsModel(hollow_bond, {
|
||||
hole_id : hole.id,
|
||||
xf : stairs_xf,
|
||||
yf : py,
|
||||
zf : stairs_zf,
|
||||
xt : stairs_xt,
|
||||
yt : py + hollow_bond.stairs_height,
|
||||
zt : stairs_zt,
|
||||
wall : wall_name,
|
||||
direction : hollow_bond.hash.stairs(hole.xf, py, hole.zf, hole.xt, py + hollow_bond.stairs_height, hole.zt, 3) < 0.5 ? -1 : 1
|
||||
}));
|
||||
|
||||
};
|
||||
|
||||
return hole.stairs;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return StairsBuilder;
|
||||
})();
|
||||
145
Public/ecma/Colliders/ElevatorCollider.ecma.js
Normal file
145
Public/ecma/Colliders/ElevatorCollider.ecma.js
Normal file
@ -0,0 +1,145 @@
|
||||
"use strict";
|
||||
|
||||
const ElevatorCollider = (function(){
|
||||
|
||||
const ElevatorCollider = function(hollow_bond, model, object_ref){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
this.object = object_ref;
|
||||
|
||||
const base_y = model.y * hollow_bond.room_height;
|
||||
const h = model.height || hollow_bond.room_height;
|
||||
const w = model.width || hollow_bond.elevator_size || 2.0;
|
||||
const d = model.depth || hollow_bond.elevator_size || 2.0;
|
||||
const th = 0.08; // Grosor de paredes
|
||||
|
||||
this.bounds = {
|
||||
min_x: model.x,
|
||||
max_x: model.x + w,
|
||||
min_z: model.z,
|
||||
max_z: model.z + d,
|
||||
min_y: base_y,
|
||||
max_y: base_y + h
|
||||
};
|
||||
|
||||
const hw = w * 0.5;
|
||||
const hd = d * 0.5;
|
||||
const door_w = hollow_bond.elevator_door_width || 1.1;
|
||||
const h_door_w = door_w * 0.5;
|
||||
|
||||
// Comprueba si un punto XZ está dentro de la base de la cabina
|
||||
this.is_inside = (px, pz) => {
|
||||
return (
|
||||
px >= self.bounds.min_x && px <= self.bounds.max_x &&
|
||||
pz >= self.bounds.min_z && pz <= self.bounds.max_z
|
||||
);
|
||||
};
|
||||
|
||||
this.resolve_player = (position, controls, player_radius = 0.22) => {
|
||||
const eye_height = controls.current_height || 1.75;
|
||||
const feet_y = position.y - eye_height;
|
||||
|
||||
// Filtro vertical de planta
|
||||
if(feet_y < self.bounds.min_y - 0.25 || feet_y > self.bounds.max_y + 0.25){
|
||||
return;
|
||||
}
|
||||
|
||||
const r = player_radius;
|
||||
const px = position.x;
|
||||
const pz = position.z;
|
||||
|
||||
// 1. Transformar a coordenadas locales de la cabina (centro = 0,0)
|
||||
const cx = (self.bounds.min_x + self.bounds.max_x) * 0.5;
|
||||
const cz = (self.bounds.min_z + self.bounds.max_z) * 0.5;
|
||||
const dx = px - cx;
|
||||
const dz = pz - cz;
|
||||
|
||||
// Rotación local inversa según orientación de la puerta (puerta siempre en +Z local)
|
||||
let lx = 0, lz = 0;
|
||||
switch(model.facing){
|
||||
case "south": lx = dx; lz = dz; break;
|
||||
case "north": lx = -dx; lz = -dz; break;
|
||||
case "east": lx = -dz; lz = dx; break;
|
||||
case "west": lx = dz; lz = -dx; break;
|
||||
}
|
||||
|
||||
// 2. Estado de apertura dinámica de las puertas
|
||||
const openness = self.object ? self.object.door_openness : 0.0;
|
||||
// Apertura libre entre las dos hojas correderas
|
||||
const free_half_gap = openness * h_door_w;
|
||||
|
||||
const is_in = (
|
||||
lx > (-hw + th) && lx < (hw - th) &&
|
||||
lz > (-hd + th) && lz < (hd - th)
|
||||
);
|
||||
|
||||
// 3. SUELO Y TECHO (si está en la cabina o pisando el umbral)
|
||||
if(is_in || (Math.abs(lx) <= h_door_w && Math.abs(lz - hd) <= 0.35)){
|
||||
const target_eye_y = self.bounds.min_y + eye_height;
|
||||
const dist_to_floor = position.y - target_eye_y;
|
||||
|
||||
if(controls.velocity_y <= 0 && dist_to_floor >= -0.35 && dist_to_floor <= 0.25){
|
||||
position.y = target_eye_y;
|
||||
controls.velocity_y = 0;
|
||||
controls.is_grounded = true;
|
||||
}
|
||||
|
||||
if(is_in && controls.velocity_y > 0 && position.y >= (self.bounds.max_y - 0.1)){
|
||||
position.y = self.bounds.max_y - 0.1;
|
||||
controls.velocity_y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. COLISIONES EN PLANO HORIZONTAL (X / Z)
|
||||
if(is_in){
|
||||
// --- DENTRO DE LA CABINA ---
|
||||
// Pared trasera (-Z)
|
||||
if(lz < (-hd + th + r)) lz = -hd + th + r;
|
||||
// Pared izquierda (-X)
|
||||
if(lx < (-hw + th + r)) lx = -hw + th + r;
|
||||
// Pared derecha (+X)
|
||||
if(lx > (hw - th - r)) lx = hw - th - r;
|
||||
|
||||
// Pared frontal (+Z): se permite cruzar únicamente si el jugador cabe en el hueco libre
|
||||
const can_pass_front = (
|
||||
(lx - r) >= -free_half_gap &&
|
||||
(lx + r) <= free_half_gap
|
||||
);
|
||||
|
||||
if(!can_pass_front && lz > (hd - th - r)){
|
||||
lz = hd - th - r;
|
||||
}
|
||||
} else {
|
||||
// --- FUERA DE LA CABINA (aproximándose desde la sala) ---
|
||||
// Si está cerca del vano frontal
|
||||
if(Math.abs(lx) < (hw + 0.2) && lz > hd && lz < (hd + 0.5)){
|
||||
const can_enter = (
|
||||
(lx - r) >= -free_half_gap &&
|
||||
(lx + r) <= free_half_gap
|
||||
);
|
||||
|
||||
// Si choca contra las hojas de la puerta o los laterales fijos del marco
|
||||
if(!can_enter && lz < (hd + r)){
|
||||
lz = hd + r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Devolver coordenadas corregidas a espacio de mundo
|
||||
let w_dx = 0, w_dz = 0;
|
||||
switch(model.facing){
|
||||
case "south": w_dx = lx; w_dz = lz; break;
|
||||
case "north": w_dx = -lx; w_dz = -lz; break;
|
||||
case "east": w_dx = lz; w_dz = -lx; break;
|
||||
case "west": w_dx = -lz; w_dz = lx; break;
|
||||
}
|
||||
|
||||
position.x = cx + w_dx;
|
||||
position.z = cz + w_dz;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return ElevatorCollider;
|
||||
})();
|
||||
75
Public/ecma/Colliders/FrameCollider.ecma.js
Normal file
75
Public/ecma/Colliders/FrameCollider.ecma.js
Normal file
@ -0,0 +1,75 @@
|
||||
"use strict";
|
||||
|
||||
const FrameCollider = (function(){
|
||||
|
||||
const FrameCollider = function(hollow_bond, model){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
|
||||
const is_horiz = model.axis === "horizontal";
|
||||
const floor_h = hollow_bond.room_height || 3;
|
||||
const pad_y = hollow_bond.room_padding || 0;
|
||||
const base_y = (model.y * floor_h) + pad_y;
|
||||
|
||||
const pass_w = model.width;
|
||||
const wall_depth = pad_y * 2;
|
||||
|
||||
const min_x = is_horiz ? model.x : model.x - wall_depth / 2;
|
||||
const max_x = is_horiz ? model.x + pass_w : model.x + wall_depth / 2;
|
||||
const min_z = is_horiz ? model.z - wall_depth / 2 : model.z;
|
||||
const max_z = is_horiz ? model.z + wall_depth / 2 : model.z + pass_w;
|
||||
|
||||
this.bounds = {
|
||||
min_x: min_x,
|
||||
max_x: max_x,
|
||||
min_z: min_z,
|
||||
max_z: max_z,
|
||||
min_y: base_y,
|
||||
max_y: base_y + model.height
|
||||
};
|
||||
|
||||
this.contains = (x, z) => {
|
||||
return (
|
||||
x >= self.bounds.min_x && x <= self.bounds.max_x &&
|
||||
z >= self.bounds.min_z && z <= self.bounds.max_z
|
||||
);
|
||||
};
|
||||
|
||||
this.resolve_player = (position, controls, player_radius = 0.22) => {
|
||||
if(!self.contains(position.x, position.z)) return;
|
||||
|
||||
const eye_height = controls.current_height || 1.75;
|
||||
|
||||
// 1. Apoyo sólido en el umbral mientras cruza el túnel
|
||||
const target_eye_y = self.bounds.min_y + eye_height;
|
||||
if(position.y <= target_eye_y){
|
||||
position.y = target_eye_y;
|
||||
controls.velocity_y = 0;
|
||||
controls.is_grounded = true;
|
||||
}
|
||||
|
||||
// 2. Dintel
|
||||
if(position.y >= self.bounds.max_y){
|
||||
position.y = self.bounds.max_y;
|
||||
if(controls.velocity_y > 0) controls.velocity_y = 0;
|
||||
}
|
||||
|
||||
// 3. Jambas laterales del marco
|
||||
if(is_horiz){
|
||||
const min_x = self.bounds.min_x + player_radius;
|
||||
const max_x = self.bounds.max_x - player_radius;
|
||||
if(position.x < min_x) position.x = min_x;
|
||||
else if(position.x > max_x) position.x = max_x;
|
||||
} else {
|
||||
const min_z = self.bounds.min_z + player_radius;
|
||||
const max_z = self.bounds.max_z - player_radius;
|
||||
if(position.z < min_z) position.z = min_z;
|
||||
else if(position.z > max_z) position.z = max_z;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return FrameCollider;
|
||||
})();
|
||||
65
Public/ecma/Colliders/RailingCollider.ecma.js
Normal file
65
Public/ecma/Colliders/RailingCollider.ecma.js
Normal file
@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
|
||||
const RailingCollider = (function(){
|
||||
|
||||
const RailingCollider = function(hollow_bond, segment){
|
||||
|
||||
const self = this;
|
||||
this.segment = segment;
|
||||
|
||||
const th = (segment.thickness || 0.08) * 0.5;
|
||||
const min_x = Math.min(segment.xf, segment.xt);
|
||||
const max_x = Math.max(segment.xf, segment.xt);
|
||||
const min_z = Math.min(segment.zf, segment.zt);
|
||||
const max_z = Math.max(segment.zf, segment.zt);
|
||||
|
||||
// Caja de colisión engrosada para impedir que el jugador la atraviese
|
||||
this.bounds = {
|
||||
min_x: (min_x === max_x) ? (min_x - th) : min_x,
|
||||
max_x: (min_x === max_x) ? (max_x + th) : max_x,
|
||||
min_z: (min_z === max_z) ? (min_z - th) : min_z,
|
||||
max_z: (min_z === max_z) ? (max_z + th) : max_z,
|
||||
min_y: segment.y,
|
||||
max_y: segment.y + (segment.height || 1.0)
|
||||
};
|
||||
|
||||
this.resolve_player = (position, controls, player_radius = 0.22) => {
|
||||
const eye_height = controls.current_height || 1.75;
|
||||
const feet_y = position.y - eye_height;
|
||||
|
||||
// Si los pies están por encima de la barandilla (salto de jugador >= 2.0m), cruza libremente
|
||||
if(feet_y >= (self.bounds.max_y - 0.08) || feet_y < (self.bounds.min_y - 0.25)){
|
||||
return;
|
||||
}
|
||||
|
||||
const r = player_radius;
|
||||
const px = position.x;
|
||||
const pz = position.z;
|
||||
|
||||
// Comprobación de contacto AABB expandido por el radio del jugador
|
||||
if(
|
||||
px + r > self.bounds.min_x && px - r < self.bounds.max_x &&
|
||||
pz + r > self.bounds.min_z && pz - r < self.bounds.max_z
|
||||
){
|
||||
// Empujar hacia el lado firme de la habitación
|
||||
switch(self.segment.side){
|
||||
case "north": // Foso hacia el Sur (+Z), suelo firme hacia el Norte (-Z)
|
||||
position.z = self.bounds.min_z - r;
|
||||
break;
|
||||
case "south": // Foso hacia el Norte (-Z), suelo firme hacia el Sur (+Z)
|
||||
position.z = self.bounds.max_z + r;
|
||||
break;
|
||||
case "west": // Foso hacia el Este (+X), suelo firme hacia el Oeste (-X)
|
||||
position.x = self.bounds.min_x - r;
|
||||
break;
|
||||
case "east": // Foso hacia el Oeste (-X), suelo firme hacia el Este (+X)
|
||||
position.x = self.bounds.max_x + r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return RailingCollider;
|
||||
})();
|
||||
193
Public/ecma/Colliders/RoomCollider.ecma.js
Normal file
193
Public/ecma/Colliders/RoomCollider.ecma.js
Normal file
@ -0,0 +1,193 @@
|
||||
"use strict";
|
||||
|
||||
const RoomCollider = (function(){
|
||||
|
||||
const RoomCollider = function(hollow_bond, model, frames = [], holes = []){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
this.frames = frames;
|
||||
this.holes = holes;
|
||||
|
||||
const pad = model.padding || 0;
|
||||
|
||||
// 1. Límites exactos del espacio de la sala (contacto continuo)
|
||||
this.room_bounds = {
|
||||
min_x: model.x,
|
||||
max_x: model.x + model.width,
|
||||
min_z: model.z,
|
||||
max_z: model.z + model.depth,
|
||||
min_y: model.y,
|
||||
max_y: model.y + model.height
|
||||
};
|
||||
|
||||
// 2. Límites de los muros interiores
|
||||
this.wall_bounds = {
|
||||
min_x: model.x + pad,
|
||||
max_x: model.x + model.width - pad,
|
||||
min_z: model.z + pad,
|
||||
max_z: model.z + model.depth - pad
|
||||
};
|
||||
|
||||
const py = Math.round(model.y / model.height);
|
||||
|
||||
// Huecos activos que perforan suelo y techo
|
||||
this.floor_holes = holes.filter(h => py > h.yf && py < h.yt);
|
||||
this.ceiling_holes = holes.filter(h => py >= h.yf && py < (h.yt - 1));
|
||||
|
||||
this.get_floor_height = () => self.room_bounds.min_y;
|
||||
|
||||
this.contains_xz = (x, z) => {
|
||||
return (
|
||||
x >= self.room_bounds.min_x && x <= self.room_bounds.max_x &&
|
||||
z >= self.room_bounds.min_z && z <= self.room_bounds.max_z
|
||||
);
|
||||
};
|
||||
|
||||
// Sin solapamiento vertical entre plantas: solo reconoce la sala correspondiente
|
||||
this.contains_3d = (x, y, z) => {
|
||||
if(!self.contains_xz(x, z)) return false;
|
||||
return (y >= (self.room_bounds.min_y - 0.2) && y < (self.room_bounds.max_y - 0.5));
|
||||
};
|
||||
|
||||
this.contains = (a, b, c) => {
|
||||
if(c !== undefined){
|
||||
return self.contains_3d(a, b, c);
|
||||
}
|
||||
return self.contains_xz(a, b);
|
||||
};
|
||||
|
||||
this.resolve_player = (position, controls, player_radius = 0.22) => {
|
||||
|
||||
const margin = 0.5;
|
||||
const is_near = (
|
||||
position.x >= (self.room_bounds.min_x - margin) && position.x <= (self.room_bounds.max_x + margin) &&
|
||||
position.z >= (self.room_bounds.min_z - margin) && position.z <= (self.room_bounds.max_z + margin)
|
||||
);
|
||||
|
||||
if(!is_near) return;
|
||||
|
||||
const eye_height = controls.current_height || 1.75;
|
||||
const floor_y = self.room_bounds.min_y;
|
||||
const ceiling_y = self.room_bounds.max_y;
|
||||
const feet_y = position.y - eye_height;
|
||||
|
||||
// Filtro estricto: si el jugador no pertenece a esta altura, no resolver nada
|
||||
if(feet_y < floor_y - 0.25 || feet_y > ceiling_y - 0.4){
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. SUELO Y TECHO
|
||||
const in_floor_area = (
|
||||
position.x >= (self.room_bounds.min_x - 0.15) && position.x <= (self.room_bounds.max_x + 0.15) &&
|
||||
position.z >= (self.room_bounds.min_z - 0.15) && position.z <= (self.room_bounds.max_z + 0.15)
|
||||
);
|
||||
|
||||
if(in_floor_area){
|
||||
|
||||
let is_over_hole = false;
|
||||
for(const h of self.floor_holes){
|
||||
const min_x = Math.min(h.xf, h.xt);
|
||||
const max_x = Math.max(h.xf, h.xt);
|
||||
const min_z = Math.min(h.zf, h.zt);
|
||||
const max_z = Math.max(h.zf, h.zt);
|
||||
|
||||
if(position.x > min_x && position.x < max_x &&
|
||||
position.z > min_z && position.z < max_z){
|
||||
is_over_hole = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Apoyo de suelo: solo si cae o está quieto (permite saltar con velocity_y > 0)
|
||||
// Apoyo de suelo: EXCLUSIVAMENTE si el jugador está a menos de 25cm por encima o debajo del suelo de ESTA sala
|
||||
if(!is_over_hole){
|
||||
const target_eye_y = floor_y + eye_height;
|
||||
const dist_to_floor = position.y - target_eye_y;
|
||||
|
||||
// Solo apoya si está cayendo/parado Y a una distancia inmediata del suelo (-0.25m a +0.10m)
|
||||
if(controls.velocity_y <= 0 && dist_to_floor >= -0.25 && dist_to_floor <= 0.10){
|
||||
position.y = target_eye_y;
|
||||
controls.velocity_y = 0;
|
||||
controls.is_grounded = true;
|
||||
} else if(dist_to_floor > 0.10){
|
||||
controls.is_grounded = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Techo: solo frena si el jugador va subiendo
|
||||
let is_under_hole = false;
|
||||
for(const h of self.ceiling_holes){
|
||||
const min_x = Math.min(h.xf, h.xt);
|
||||
const max_x = Math.max(h.xf, h.xt);
|
||||
const min_z = Math.min(h.zf, h.zt);
|
||||
const max_z = Math.max(h.zf, h.zt);
|
||||
|
||||
if(position.x > min_x && position.x < max_x &&
|
||||
position.z > min_z && position.z < max_z){
|
||||
is_under_hole = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!is_under_hole && controls.velocity_y > 0 && position.y >= ceiling_y){
|
||||
position.y = ceiling_y;
|
||||
controls.velocity_y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. PAREDES Y PUERTAS
|
||||
const min_allowed_x = self.wall_bounds.min_x + player_radius;
|
||||
const max_allowed_x = self.wall_bounds.max_x - player_radius;
|
||||
const min_allowed_z = self.wall_bounds.min_z + player_radius;
|
||||
const max_allowed_z = self.wall_bounds.max_z - player_radius;
|
||||
|
||||
let in_door_north = false, in_door_south = false, in_door_west = false, in_door_east = false;
|
||||
|
||||
for(const f of self.frames){
|
||||
if(f.axis === "horizontal"){
|
||||
if(position.x >= (f.x - 0.05) && position.x <= (f.x + f.width + 0.05)){
|
||||
if(Math.abs(f.z - model.z) < Math.abs(f.z - (model.z + model.depth))){
|
||||
in_door_north = true;
|
||||
} else {
|
||||
in_door_south = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(position.z >= (f.z - 0.05) && position.z <= (f.z + f.width + 0.05)){
|
||||
if(Math.abs(f.x - model.x) < Math.abs(f.x - (model.x + model.width))){
|
||||
in_door_west = true;
|
||||
} else {
|
||||
in_door_east = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!in_door_west && position.x < min_allowed_x && position.x >= self.room_bounds.min_x - 0.1){
|
||||
position.x = min_allowed_x;
|
||||
}
|
||||
if(!in_door_east && position.x > max_allowed_x && position.x <= self.room_bounds.max_x + 0.1){
|
||||
position.x = max_allowed_x;
|
||||
}
|
||||
|
||||
if(!in_door_north && position.z < min_allowed_z && position.z >= self.room_bounds.min_z - 0.1){
|
||||
position.z = min_allowed_z;
|
||||
}
|
||||
if(!in_door_south && position.z > max_allowed_z && position.z <= self.room_bounds.max_z + 0.1){
|
||||
position.z = max_allowed_z;
|
||||
}
|
||||
|
||||
// 3. BARANDILLAS (bloquean el borde del hueco salvo si se salta >= 1.0m o por descansillo)
|
||||
if(self.railings){
|
||||
for(let i = 0; i < self.railings.length; i++){
|
||||
self.railings[i].resolve_player(position, controls, player_radius);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return RoomCollider;
|
||||
})();
|
||||
100
Public/ecma/Colliders/StairCollider.ecma.js
Normal file
100
Public/ecma/Colliders/StairCollider.ecma.js
Normal file
@ -0,0 +1,100 @@
|
||||
"use strict";
|
||||
|
||||
const StairCollider = (function(){
|
||||
|
||||
const StairCollider = function(hollow_bond, model){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
|
||||
const bounds = model.get_world_bounds();
|
||||
|
||||
const is_x = (model.wall === "north" || model.wall === "south");
|
||||
const length = is_x ? model.width : model.depth;
|
||||
const landing_size = hollow_bond.stairs_landing_size || 1.0;
|
||||
const flight_len = Math.max(1.0, length - (landing_size * 2));
|
||||
const rise = hollow_bond.room_height;
|
||||
const dir = model.direction;
|
||||
|
||||
this.bounds = {
|
||||
min_x: bounds.x,
|
||||
max_x: bounds.x + bounds.width,
|
||||
min_y: bounds.y,
|
||||
max_y: bounds.y + rise,
|
||||
min_z: bounds.z,
|
||||
max_z: bounds.z + bounds.depth
|
||||
};
|
||||
|
||||
this.resolve_player = (position, controls, player_radius = 0.22) => {
|
||||
const eye_height = controls.current_height || 1.75;
|
||||
const px = position.x;
|
||||
const py = position.y - eye_height;
|
||||
const pz = position.z;
|
||||
|
||||
// Filtro general AABB
|
||||
if(
|
||||
px < self.bounds.min_x - player_radius || px > self.bounds.max_x + player_radius ||
|
||||
pz < self.bounds.min_z - player_radius || pz > self.bounds.max_z + player_radius ||
|
||||
py < self.bounds.min_y - 0.4 || py > self.bounds.max_y + 1.2
|
||||
){
|
||||
return;
|
||||
}
|
||||
|
||||
// Progresión longitudinal a lo largo de la escalera (0 a 1)
|
||||
const coord = is_x ? (px - self.bounds.min_x) : (pz - self.bounds.min_z);
|
||||
|
||||
let expected_y = self.bounds.min_y;
|
||||
|
||||
if(dir === 1){
|
||||
if(coord <= landing_size){
|
||||
expected_y = self.bounds.min_y;
|
||||
} else if(coord >= (length - landing_size)){
|
||||
expected_y = self.bounds.max_y;
|
||||
} else {
|
||||
const t = (coord - landing_size) / flight_len;
|
||||
expected_y = self.bounds.min_y + t * rise;
|
||||
}
|
||||
} else {
|
||||
if(coord <= landing_size){
|
||||
expected_y = self.bounds.max_y;
|
||||
} else if(coord >= (length - landing_size)){
|
||||
expected_y = self.bounds.min_y;
|
||||
} else {
|
||||
const t = (length - landing_size - coord) / flight_len;
|
||||
expected_y = self.bounds.min_y + t * rise;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. PISADA (Subida suave de rampa)
|
||||
const dist_to_stair = py - expected_y;
|
||||
if(controls.velocity_y <= 0 && dist_to_stair >= -0.25 && dist_to_stair <= 0.25){
|
||||
position.y = expected_y + eye_height;
|
||||
controls.velocity_y = 0;
|
||||
controls.is_grounded = true;
|
||||
}
|
||||
|
||||
// 2. COLISIÓN CONTRA BARANDILLA INTERIOR
|
||||
// Si el jugador no salta por encima de 1.0m, se le bloquea la salida lateral hacia el hueco
|
||||
if(py < (expected_y + 1.0)){
|
||||
const r = player_radius;
|
||||
switch(model.wall){
|
||||
case "north": // Hueco hacia +Z
|
||||
if(position.z > self.bounds.max_z - r) position.z = self.bounds.max_z - r;
|
||||
break;
|
||||
case "south": // Hueco hacia -Z
|
||||
if(position.z < self.bounds.min_z + r) position.z = self.bounds.min_z + r;
|
||||
break;
|
||||
case "west": // Hueco hacia +X
|
||||
if(position.x > self.bounds.max_x - r) position.x = self.bounds.max_x - r;
|
||||
break;
|
||||
case "east": // Hueco hacia -X
|
||||
if(position.x < self.bounds.min_x + r) position.x = self.bounds.min_x + r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return StairCollider;
|
||||
})();
|
||||
161
Public/ecma/Controls/DebugControls.ecma.js
Normal file
161
Public/ecma/Controls/DebugControls.ecma.js
Normal file
@ -0,0 +1,161 @@
|
||||
"use strict";
|
||||
|
||||
const DebugControls = (function(){
|
||||
|
||||
const DebugControls = function(hollow_bond){
|
||||
|
||||
const self = this,
|
||||
euler = new THREE.Euler(0, 0, 0, "YXZ"),
|
||||
keys = {
|
||||
forward : false,
|
||||
backward : false,
|
||||
left : false,
|
||||
right : false,
|
||||
up : false,
|
||||
down : false
|
||||
};
|
||||
let is_pointer_locked = false;
|
||||
|
||||
this.setup = inspector => {
|
||||
|
||||
const canvas = inspector.renderer.domElement;
|
||||
|
||||
canvas.addEventListener("click", () => canvas.requestPointerLock());
|
||||
document.addEventListener("pointerlockchange", () => {
|
||||
is_pointer_locked = (document.pointerLockElement === canvas);
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", evt => {
|
||||
if(!is_pointer_locked) return;
|
||||
euler.setFromQuaternion(inspector.camera.quaternion);
|
||||
euler.y -= evt.movementX * 0.002;
|
||||
euler.x -= evt.movementY * 0.002;
|
||||
euler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, euler.x));
|
||||
inspector.camera.quaternion.setFromEuler(euler);
|
||||
});
|
||||
|
||||
window.addEventListener("keydown", evt => {
|
||||
// Teclas de movimiento
|
||||
switch(evt.code){
|
||||
case "KeyW":
|
||||
case "ArrowUp":
|
||||
keys.forward = true;
|
||||
break;
|
||||
case "KeyS":
|
||||
case "ArrowDown":
|
||||
keys.backward = true;
|
||||
break;
|
||||
case "KeyA":
|
||||
case "ArrowLeft":
|
||||
keys.left = true;
|
||||
break;
|
||||
case "KeyD":
|
||||
case "ArrowRight":
|
||||
keys.right = true;
|
||||
break;
|
||||
case "Space":
|
||||
keys.up = true;
|
||||
break;
|
||||
case "ShiftLeft":
|
||||
case "ShiftRight":
|
||||
case "KeyC":
|
||||
keys.down = true;
|
||||
break;
|
||||
// Cambio de estrato con Q y E
|
||||
case "KeyE":
|
||||
inspector.set_floor(inspector.current_floor + 1);
|
||||
break;
|
||||
case "KeyQ":
|
||||
inspector.set_floor(inspector.current_floor - 1);
|
||||
break;
|
||||
// Rango de capas con las teclas numéricas 1 a 5
|
||||
case "Digit0":
|
||||
inspector.vertical_layers_range = 0;
|
||||
inspector.update_layer_visibility();
|
||||
inspector.update_hud();
|
||||
break;
|
||||
case "Digit1":
|
||||
inspector.vertical_layers_range = 1;
|
||||
inspector.update_layer_visibility();
|
||||
inspector.update_hud();
|
||||
break;
|
||||
case "Digit2":
|
||||
inspector.vertical_layers_range = 2;
|
||||
inspector.update_layer_visibility();
|
||||
inspector.update_hud();
|
||||
break;
|
||||
case "Digit3":
|
||||
inspector.vertical_layers_range = 3;
|
||||
inspector.update_layer_visibility();
|
||||
inspector.update_hud();
|
||||
break;
|
||||
case "Digit4":
|
||||
inspector.vertical_layers_range = 4;
|
||||
inspector.update_layer_visibility();
|
||||
inspector.update_hud();
|
||||
break;
|
||||
};
|
||||
});
|
||||
|
||||
window.addEventListener("keyup", evt => {
|
||||
switch(evt.code){
|
||||
case "KeyW":
|
||||
case "ArrowUp":
|
||||
keys.forward = false;
|
||||
break;
|
||||
case "KeyS":
|
||||
case "ArrowDown":
|
||||
keys.backward = false;
|
||||
break;
|
||||
case "KeyA":
|
||||
case "ArrowLeft":
|
||||
keys.left = false;
|
||||
break;
|
||||
case "KeyD":
|
||||
case "ArrowRight":
|
||||
keys.right = false;
|
||||
break;
|
||||
case "Space":
|
||||
keys.up = false;
|
||||
break;
|
||||
case "ShiftLeft":
|
||||
case "ShiftRight":
|
||||
case "KeyC":
|
||||
keys.down = false;
|
||||
break;
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
this.update_movement = (inspector, delta) => {
|
||||
if(!is_pointer_locked)
|
||||
return;
|
||||
|
||||
const movement = new THREE.Vector3();
|
||||
|
||||
if(keys.forward)
|
||||
movement.z --;
|
||||
if(keys.backward)
|
||||
movement.z ++;
|
||||
if(keys.left)
|
||||
movement.x --;
|
||||
if(keys.right)
|
||||
movement.x ++;
|
||||
|
||||
if(movement.lengthSq() > 0){
|
||||
movement.normalize();
|
||||
movement.applyQuaternion(inspector.camera.quaternion);
|
||||
};
|
||||
|
||||
if(keys.up)
|
||||
movement.y ++;
|
||||
if(keys.down)
|
||||
movement.y --;
|
||||
|
||||
inspector.camera.position.addScaledVector(movement, 25 * delta);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return DebugControls;
|
||||
})();
|
||||
160
Public/ecma/Controls/GameControls.ecma.js
Normal file
160
Public/ecma/Controls/GameControls.ecma.js
Normal file
@ -0,0 +1,160 @@
|
||||
"use strict";
|
||||
|
||||
const GameControls = (function(){
|
||||
|
||||
const GameControls = function(hollow_bond){
|
||||
|
||||
const self = this,
|
||||
euler = new THREE.Euler(0, 0, 0, "YXZ"),
|
||||
keys = {
|
||||
forward: false,
|
||||
backward: false,
|
||||
left: false,
|
||||
right: false,
|
||||
sprint: false,
|
||||
crouch: false,
|
||||
jump: false
|
||||
};
|
||||
|
||||
let is_pointer_locked = false;
|
||||
|
||||
this.standing_height = 1.75;
|
||||
this.player_width = this.standing_height / 4;
|
||||
this.player_radius = this.player_width / 2;
|
||||
this.crouch_height = this.standing_height / 2;
|
||||
this.current_height = this.standing_height;
|
||||
|
||||
this.walk_speed = 3.5;
|
||||
this.sprint_speed = 6.2;
|
||||
this.crouch_speed = 1.8;
|
||||
|
||||
this.velocity_y = 0;
|
||||
this.is_grounded = false;
|
||||
this.is_crouching = false;
|
||||
|
||||
this.set_dimensions = height => {
|
||||
self.standing_height = height || 1.75;
|
||||
self.player_width = self.standing_height / 4;
|
||||
self.player_radius = self.player_width / 2;
|
||||
self.crouch_height = self.standing_height / 2;
|
||||
self.current_height = self.standing_height;
|
||||
};
|
||||
|
||||
this.setup = dungeon => {
|
||||
const canvas = dungeon.renderer.domElement;
|
||||
|
||||
canvas.addEventListener("click", () => canvas.requestPointerLock());
|
||||
document.addEventListener("pointerlockchange", () => {
|
||||
is_pointer_locked = (document.pointerLockElement === canvas);
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", evt => {
|
||||
if(!is_pointer_locked) return;
|
||||
euler.setFromQuaternion(dungeon.camera.quaternion);
|
||||
euler.y -= evt.movementX * 0.002;
|
||||
euler.x -= evt.movementY * 0.002;
|
||||
euler.x = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, euler.x));
|
||||
dungeon.camera.quaternion.setFromEuler(euler);
|
||||
});
|
||||
|
||||
window.addEventListener("keydown", evt => {
|
||||
switch(evt.code){
|
||||
case "KeyW": case "ArrowUp": keys.forward = true; break;
|
||||
case "KeyS": case "ArrowDown": keys.backward = true; break;
|
||||
case "KeyA": case "ArrowLeft": keys.left = true; break;
|
||||
case "KeyD": case "ArrowRight": keys.right = true; break;
|
||||
case "ShiftLeft": case "ShiftRight": keys.sprint = true; break;
|
||||
case "KeyC": case "ControlLeft": keys.crouch = true; break;
|
||||
case "Space": keys.jump = true; break;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("keyup", evt => {
|
||||
switch(evt.code){
|
||||
case "KeyW": case "ArrowUp": keys.forward = false; break;
|
||||
case "KeyS": case "ArrowDown": keys.backward = false; break;
|
||||
case "KeyA": case "ArrowLeft": keys.left = false; break;
|
||||
case "KeyD": case "ArrowRight": keys.right = false; break;
|
||||
case "ShiftLeft": case "ShiftRight": keys.sprint = false; break;
|
||||
case "KeyC": case "ControlLeft": keys.crouch = false; break;
|
||||
case "Space": keys.jump = false; break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.update = (dungeon, delta) => {
|
||||
if(!is_pointer_locked) return;
|
||||
|
||||
const gravity = hollow_bond.gravity || 9.81;
|
||||
|
||||
// 1. Estado de agachado
|
||||
self.is_crouching = keys.crouch;
|
||||
const target_height = self.is_crouching ? self.crouch_height : self.standing_height;
|
||||
self.current_height += (target_height - self.current_height) * Math.min(1, 14 * delta);
|
||||
|
||||
// 2. Velocidad de traslación
|
||||
let current_speed = self.walk_speed;
|
||||
if(self.is_crouching){
|
||||
current_speed = self.crouch_speed;
|
||||
} else if(keys.sprint){
|
||||
current_speed = self.sprint_speed;
|
||||
}
|
||||
|
||||
// 3. Movimiento horizontal en XZ
|
||||
const input_dir = new THREE.Vector3();
|
||||
if(keys.forward) input_dir.z -= 1;
|
||||
if(keys.backward) input_dir.z += 1;
|
||||
if(keys.left) input_dir.x -= 1;
|
||||
if(keys.right) input_dir.x += 1;
|
||||
|
||||
if(input_dir.lengthSq() > 0){
|
||||
input_dir.normalize();
|
||||
const move_dir = new THREE.Vector3(input_dir.x, 0, input_dir.z);
|
||||
move_dir.applyAxisAngle(new THREE.Vector3(0, 1, 0), euler.y);
|
||||
|
||||
dungeon.camera.position.x += move_dir.x * current_speed * delta;
|
||||
dungeon.camera.position.z += move_dir.z * current_speed * delta;
|
||||
}
|
||||
|
||||
// 4. Salto
|
||||
if(self.is_grounded && keys.jump){
|
||||
const jump_height = self.standing_height / 2;
|
||||
self.velocity_y = Math.sqrt(2 * gravity * jump_height);
|
||||
self.is_grounded = false;
|
||||
}
|
||||
|
||||
// 5. Aplicar gravedad en el aire
|
||||
self.velocity_y -= gravity * delta;
|
||||
dungeon.camera.position.y += self.velocity_y * delta;
|
||||
|
||||
// Reiniciamos apoyo; sólo un suelo bajo los pies lo activará
|
||||
self.is_grounded = false;
|
||||
|
||||
// 6. Comprobación de apoyo en la sala actual
|
||||
if(dungeon.current_room && dungeon.current_room.collider){
|
||||
dungeon.current_room.collider.resolve_player(dungeon.camera.position, self, self.player_radius);
|
||||
}
|
||||
|
||||
// 7. Comprobación de marcos (solo de la sala actual y a la misma altura)
|
||||
if(dungeon.current_room && dungeon.current_room.model && dungeon.current_room.model.frames){
|
||||
const feet_y = dungeon.camera.position.y - self.current_height;
|
||||
|
||||
for(const fid of dungeon.current_room.model.frames){
|
||||
const f_obj = dungeon.objects.frame ? dungeon.objects.frame[fid] : null;
|
||||
if(!f_obj || !f_obj.collider) continue;
|
||||
|
||||
// Comprobamos XZ y verificamos que el marco esté en este mismo piso (+- 1 metro)
|
||||
const frame_y = f_obj.model ? f_obj.model.y : 0;
|
||||
if(Math.abs(feet_y - frame_y) < 1.0){
|
||||
if(f_obj.collider.contains(dungeon.camera.position.x, dungeon.camera.position.z)){
|
||||
f_obj.collider.resolve_player(dungeon.camera.position, self, self.player_radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return GameControls;
|
||||
})();
|
||||
33
Public/ecma/Geometries/Debug/ElevatorGeometryDebug.ecma.js
Normal file
33
Public/ecma/Geometries/Debug/ElevatorGeometryDebug.ecma.js
Normal file
@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
|
||||
const ElevatorGeometryDebug = (function(){
|
||||
|
||||
const ElevatorGeometryDebug = function(hollow_bond){
|
||||
|
||||
let instance = null;
|
||||
|
||||
/**
|
||||
* Devuelve la geometría única compartida para las cabinas de ascensor.
|
||||
*/
|
||||
this.get = () => {
|
||||
if(!instance){
|
||||
const size = hollow_bond.elevator_size || 2,
|
||||
floor_height = hollow_bond.room_height || 3,
|
||||
height = floor_height * 0.9;
|
||||
|
||||
instance = new THREE.BoxGeometry(size, height, size);
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
if(instance){
|
||||
instance.dispose();
|
||||
instance = null;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return ElevatorGeometryDebug;
|
||||
})();
|
||||
38
Public/ecma/Geometries/Debug/FrameGeometryDebug.ecma.js
Normal file
38
Public/ecma/Geometries/Debug/FrameGeometryDebug.ecma.js
Normal file
@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
|
||||
const FrameGeometryDebug = (function(){
|
||||
|
||||
const FrameGeometryDebug = function(hollow_bond){
|
||||
|
||||
const cache = {};
|
||||
|
||||
/**
|
||||
* Obtiene o instancia una geometría de caja con las dimensiones exactas del marco.
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @param {number} depth
|
||||
* @returns {THREE.BoxGeometry}
|
||||
*/
|
||||
this.get = (width, height, depth) => {
|
||||
const key = `${width}_${height}_${depth}`;
|
||||
|
||||
if(!cache[key]){
|
||||
cache[key] = new THREE.BoxGeometry(width, height, depth);
|
||||
}
|
||||
|
||||
return cache[key];
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const key in cache){
|
||||
if(cache[key]){
|
||||
cache[key].dispose();
|
||||
delete cache[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return FrameGeometryDebug;
|
||||
})();
|
||||
42
Public/ecma/Geometries/Debug/HoleGeometryDebug.ecma.js
Normal file
42
Public/ecma/Geometries/Debug/HoleGeometryDebug.ecma.js
Normal file
@ -0,0 +1,42 @@
|
||||
"use strict";
|
||||
|
||||
const HoleGeometryDebug = (function(){
|
||||
|
||||
const HoleGeometryDebug = function(hollow_bond){
|
||||
|
||||
const cache = {};
|
||||
const floor_height = hollow_bond.room_height || 3;
|
||||
|
||||
/**
|
||||
* Obtiene o genera la caja 3D correspondiente al vano del hueco.
|
||||
* @param {number} width - Ancho en X (xt - xf)
|
||||
* @param {number} floors - Pisos perforados (yt - yf)
|
||||
* @param {number} depth - Fondo en Z (zt - zf)
|
||||
*/
|
||||
this.get = (width, floors, depth) => {
|
||||
const height = floors * floor_height,
|
||||
key = `${width}_${height}_${depth}`;
|
||||
|
||||
if(!cache[key]){
|
||||
cache[key] = new THREE.BoxGeometry(width, height, depth);
|
||||
}
|
||||
|
||||
return cache[key];
|
||||
};
|
||||
|
||||
/**
|
||||
* Libera todas las geometrías de huecos cacheadas en la GPU.
|
||||
*/
|
||||
this.reset = () => {
|
||||
for(const key in cache){
|
||||
if(cache[key]){
|
||||
cache[key].dispose();
|
||||
delete cache[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return HoleGeometryDebug;
|
||||
})();
|
||||
38
Public/ecma/Geometries/Debug/RoomGeometryDebug.ecma.js
Normal file
38
Public/ecma/Geometries/Debug/RoomGeometryDebug.ecma.js
Normal file
@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
|
||||
const RoomGeometryDebug = (function(){
|
||||
|
||||
const RoomGeometryDebug = function(hollow_bond){
|
||||
|
||||
const cache = {};
|
||||
|
||||
/**
|
||||
* Obtiene o crea una geometría de caja ortoédrica pura.
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @param {number} depth
|
||||
* @returns {THREE.BoxGeometry}
|
||||
*/
|
||||
this.get = (width, height, depth) => {
|
||||
const key = `${width}_${height}_${depth}`;
|
||||
|
||||
if(!cache[key]){
|
||||
cache[key] = new THREE.BoxGeometry(width, height, depth);
|
||||
}
|
||||
|
||||
return cache[key];
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const key in cache){
|
||||
if(cache[key]){
|
||||
cache[key].dispose();
|
||||
delete cache[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return RoomGeometryDebug;
|
||||
})();
|
||||
52
Public/ecma/Geometries/Debug/StairGeometryDebug.ecma.js
Normal file
52
Public/ecma/Geometries/Debug/StairGeometryDebug.ecma.js
Normal file
@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
|
||||
const StairGeometryDebug = (function(){
|
||||
|
||||
const StairGeometryDebug = function(hollow_bond){
|
||||
|
||||
const cache = {};
|
||||
const floor_height = hollow_bond.room_height || 3;
|
||||
|
||||
this.get = (width, depth) => {
|
||||
const key = `${width}_${floor_height}_${depth}`;
|
||||
|
||||
if(!cache[key]){
|
||||
const landing_thickness = 0.15,
|
||||
landing_length = Math.max(1, depth * 0.25),
|
||||
ramp_run = depth - (landing_length * 2),
|
||||
ramp_rise = floor_height,
|
||||
ramp_length = Math.sqrt(ramp_run * ramp_run + ramp_rise * ramp_rise);
|
||||
|
||||
cache[key] = {
|
||||
landing_bottom : new THREE.BoxGeometry(width, landing_thickness, landing_length),
|
||||
landing_top : new THREE.BoxGeometry(width, landing_thickness, landing_length),
|
||||
ramp : new THREE.BoxGeometry(width, landing_thickness, ramp_length),
|
||||
specs : {
|
||||
landing_length,
|
||||
landing_thickness,
|
||||
ramp_run,
|
||||
ramp_rise,
|
||||
ramp_length,
|
||||
angle : Math.atan2(ramp_rise, ramp_run)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return cache[key];
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const key in cache){
|
||||
if(cache[key]){
|
||||
cache[key].landing_bottom.dispose();
|
||||
cache[key].landing_top.dispose();
|
||||
cache[key].ramp.dispose();
|
||||
delete cache[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return StairGeometryDebug;
|
||||
})();
|
||||
130
Public/ecma/Geometries/Game/ElevatorFrameGeometryGame.ecma.js
Normal file
130
Public/ecma/Geometries/Game/ElevatorFrameGeometryGame.ecma.js
Normal file
@ -0,0 +1,130 @@
|
||||
"use strict";
|
||||
|
||||
const ElevatorFrameGeometryGame = (function(){
|
||||
|
||||
const ElevatorFrameGeometryGame = function(hollow_bond){
|
||||
|
||||
const cache = {};
|
||||
|
||||
// Añade una caja completa (6 caras) para elementos aislados o interiores
|
||||
const append_box = (buffers, min_x, max_x, min_y, max_y, min_z, max_z) => {
|
||||
const { positions, normals, uvs, indices } = buffers;
|
||||
let offset = positions.length / 3;
|
||||
|
||||
const add_quad = (p1, p2, p3, p4, n) => {
|
||||
positions.push(...p1, ...p2, ...p3, ...p4);
|
||||
normals.push(...n, ...n, ...n, ...n);
|
||||
uvs.push(0, 0, 1, 0, 1, 1, 0, 1);
|
||||
indices.push(offset, offset + 2, offset + 1, offset, offset + 3, offset + 2);
|
||||
offset += 4;
|
||||
};
|
||||
|
||||
add_quad([min_x, max_y, max_z], [max_x, max_y, max_z], [max_x, max_y, min_z], [min_x, max_y, min_z], [0, 1, 0]);
|
||||
add_quad([min_x, min_y, min_z], [max_x, min_y, min_z], [max_x, min_y, max_z], [min_x, min_y, max_z], [0, -1, 0]);
|
||||
add_quad([max_x, min_y, min_z], [min_x, min_y, min_z], [min_x, max_y, min_z], [max_x, max_y, min_z], [0, 0, -1]);
|
||||
add_quad([min_x, min_y, max_z], [max_x, min_y, max_z], [max_x, max_y, max_z], [min_x, max_y, max_z], [0, 0, 1]);
|
||||
add_quad([min_x, min_y, min_z], [min_x, min_y, max_z], [min_x, max_y, max_z], [min_x, max_y, min_z], [-1, 0, 0]);
|
||||
add_quad([max_x, min_y, max_z], [max_x, min_y, min_z], [max_x, max_y, min_z], [max_x, max_y, max_z], [1, 0, 0]);
|
||||
};
|
||||
|
||||
// Añade una moldura apoyada en pared SIN cara trasera (-Z) para evitar Z-fighting
|
||||
const append_relief = (buffers, min_x, max_x, min_y, max_y, z_base, z_front) => {
|
||||
const { positions, normals, uvs, indices } = buffers;
|
||||
let offset = positions.length / 3;
|
||||
|
||||
const add_quad = (p1, p2, p3, p4, n) => {
|
||||
positions.push(...p1, ...p2, ...p3, ...p4);
|
||||
normals.push(...n, ...n, ...n, ...n);
|
||||
uvs.push(0, 0, 1, 0, 1, 1, 0, 1);
|
||||
indices.push(offset, offset + 2, offset + 1, offset, offset + 3, offset + 2);
|
||||
offset += 4;
|
||||
};
|
||||
|
||||
// Frontal (+Z) hacia la sala
|
||||
add_quad([min_x, min_y, z_front], [max_x, min_y, z_front], [max_x, max_y, z_front], [min_x, max_y, z_front], [0, 0, 1]);
|
||||
// Superior (+Y)
|
||||
add_quad([min_x, max_y, z_front], [max_x, max_y, z_front], [max_x, max_y, z_base], [min_x, max_y, z_base], [0, 1, 0]);
|
||||
// Inferior (-Y)
|
||||
add_quad([min_x, min_y, z_base], [max_x, min_y, z_base], [max_x, min_y, z_front], [min_x, min_y, z_front], [0, -1, 0]);
|
||||
// Lateral Izquierdo (-X)
|
||||
add_quad([min_x, min_y, z_base], [min_x, min_y, z_front], [min_x, max_y, z_front], [min_x, max_y, z_base], [-1, 0, 0]);
|
||||
// Lateral Derecho (+X)
|
||||
add_quad([max_x, min_y, z_front], [max_x, min_y, z_base], [max_x, max_y, z_base], [max_x, max_y, z_front], [1, 0, 0]);
|
||||
// NOTA: No generamos la cara -Z (trasera) porque apoya directamente sobre la pared.
|
||||
};
|
||||
|
||||
this.get_for_elevator = model => {
|
||||
const door_w = hollow_bond.elevator_door_width || 1.1;
|
||||
const door_h = 2.10;
|
||||
const key = `frame_clean_${door_w}_${door_h}`;
|
||||
if(cache[key]) return cache[key];
|
||||
|
||||
const buffers = { positions: [], normals: [], uvs: [], indices: [] };
|
||||
const hw = door_w * 0.5; // 0.55m
|
||||
|
||||
const wall_z = (hollow_bond.room_padding !== undefined) ? hollow_bond.room_padding : 0.0;
|
||||
const trim_w = 0.12; // 12 cm de moldura
|
||||
const header_h = 0.22; // 22 cm de dintel
|
||||
const z_front = wall_z + 0.02; // Relieve limpio de 2 cm
|
||||
|
||||
// --- GRUPO 0: Acero cepillado del marco ---
|
||||
const start_metal = buffers.indices.length;
|
||||
|
||||
// 1. Forro interior de las jambas (dentro del vano, sin pared de yeso)
|
||||
append_box(buffers, -hw - 0.025, -hw, 0, door_h, -0.04, wall_z);
|
||||
append_box(buffers, hw, hw + 0.025, 0, door_h, -0.04, wall_z);
|
||||
append_box(buffers, -hw, hw, door_h, door_h + 0.025, -0.04, wall_z);
|
||||
|
||||
// 2. Tapajuntas sobre la pared (SIN penetración y SIN cara trasera)
|
||||
// Moldura izquierda
|
||||
append_relief(buffers, -hw - trim_w, -hw, 0, door_h + header_h, wall_z, z_front);
|
||||
// Moldura derecha
|
||||
append_relief(buffers, hw, hw + trim_w, 0, door_h + header_h, wall_z, z_front);
|
||||
// Dintel superior
|
||||
append_relief(buffers, -hw, hw, door_h, door_h + header_h, wall_z, z_front);
|
||||
|
||||
// 3. Umbral de suelo
|
||||
append_box(buffers, -hw, hw, 0, 0.012, -0.04, z_front);
|
||||
|
||||
// 4. Placa base de la botonera exterior (en relieve, sin cara trasera)
|
||||
const btn_x1 = hw + trim_w + 0.03;
|
||||
const btn_x2 = btn_x1 + 0.12;
|
||||
append_relief(buffers, btn_x1, btn_x2, 1.10, 1.38, wall_z, z_front);
|
||||
|
||||
const count_metal = buffers.indices.length - start_metal;
|
||||
|
||||
// --- GRUPO 1: Elementos luminosos ---
|
||||
const start_light = buffers.indices.length;
|
||||
|
||||
// Display de piso
|
||||
append_relief(buffers, -0.18, 0.18, door_h + 0.06, door_h + 0.16, z_front, z_front + 0.006);
|
||||
|
||||
// Botón de llamada iluminado
|
||||
append_relief(buffers, btn_x1 + 0.035, btn_x2 - 0.035, 1.20, 1.28, z_front, z_front + 0.008);
|
||||
|
||||
const count_light = buffers.indices.length - start_light;
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(buffers.positions, 3));
|
||||
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(buffers.normals, 3));
|
||||
geometry.setAttribute("uv", new THREE.Float32BufferAttribute(buffers.uvs, 2));
|
||||
geometry.setIndex(buffers.indices);
|
||||
|
||||
geometry.addGroup(start_metal, count_metal, 0);
|
||||
geometry.addGroup(start_light, count_light, 1);
|
||||
|
||||
cache[key] = geometry;
|
||||
return geometry;
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const k in cache){
|
||||
cache[k].dispose();
|
||||
delete cache[k];
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return ElevatorFrameGeometryGame;
|
||||
})();
|
||||
122
Public/ecma/Geometries/Game/ElevatorGeometryGame.ecma.js
Normal file
122
Public/ecma/Geometries/Game/ElevatorGeometryGame.ecma.js
Normal file
@ -0,0 +1,122 @@
|
||||
"use strict";
|
||||
|
||||
const ElevatorGeometryGame = (function(){
|
||||
|
||||
const ElevatorGeometryGame = function(hollow_bond){
|
||||
|
||||
const cache = {};
|
||||
|
||||
const append_box = (buffers, min_x, max_x, min_y, max_y, min_z, max_z) => {
|
||||
const { positions, normals, uvs, indices } = buffers;
|
||||
let offset = positions.length / 3;
|
||||
|
||||
const add_quad = (p1, p2, p3, p4, n) => {
|
||||
positions.push(...p1, ...p2, ...p3, ...p4);
|
||||
normals.push(...n, ...n, ...n, ...n);
|
||||
uvs.push(0, 0, 1, 0, 1, 1, 0, 1);
|
||||
indices.push(offset, offset + 2, offset + 1, offset, offset + 3, offset + 2);
|
||||
offset += 4;
|
||||
};
|
||||
|
||||
add_quad([min_x, max_y, max_z], [max_x, max_y, max_z], [max_x, max_y, min_z], [min_x, max_y, min_z], [0, 1, 0]);
|
||||
add_quad([min_x, min_y, min_z], [max_x, min_y, min_z], [max_x, min_y, max_z], [min_x, min_y, max_z], [0, -1, 0]);
|
||||
add_quad([max_x, min_y, min_z], [min_x, min_y, min_z], [min_x, max_y, min_z], [max_x, max_y, min_z], [0, 0, -1]);
|
||||
add_quad([min_x, min_y, max_z], [max_x, min_y, max_z], [max_x, max_y, max_z], [min_x, max_y, max_z], [0, 0, 1]);
|
||||
add_quad([min_x, min_y, min_z], [min_x, min_y, max_z], [min_x, max_y, max_z], [min_x, max_y, min_z], [-1, 0, 0]);
|
||||
add_quad([max_x, min_y, max_z], [max_x, min_y, min_z], [max_x, max_y, min_z], [max_x, max_y, max_z], [1, 0, 0]);
|
||||
};
|
||||
|
||||
const create_buffer_geometry = buffers => {
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute("position", new THREE.Float32BufferAttribute(buffers.positions, 3));
|
||||
geo.setAttribute("normal", new THREE.Float32BufferAttribute(buffers.normals, 3));
|
||||
geo.setAttribute("uv", new THREE.Float32BufferAttribute(buffers.uvs, 2));
|
||||
geo.setIndex(buffers.indices);
|
||||
return geo;
|
||||
};
|
||||
|
||||
this.get_cabin_geometry = model => {
|
||||
const key = `cabin_${model.width}_${model.height}`;
|
||||
if(cache[key]) return cache[key];
|
||||
|
||||
const buffers = { positions: [], normals: [], uvs: [], indices: [] };
|
||||
|
||||
const w = model.width || hollow_bond.elevator_size || 2.0;
|
||||
const h = model.height || hollow_bond.room_height || 3.0;
|
||||
const d = model.depth || hollow_bond.elevator_size || 2.0;
|
||||
const hw = w * 0.5;
|
||||
const hd = d * 0.5;
|
||||
const th = 0.08;
|
||||
const door_w = 1.1;
|
||||
const door_h = 2.15;
|
||||
const h_door_w = door_w * 0.5;
|
||||
|
||||
// 1. Suelo y Techo
|
||||
append_box(buffers, -hw, hw, 0, th, -hd, hd);
|
||||
append_box(buffers, -hw, hw, h - th, h, -hd, hd);
|
||||
|
||||
// 2. Pared trasera (-Z)
|
||||
append_box(buffers, -hw, hw, th, h - th, -hd, -hd + th);
|
||||
|
||||
// 3. Paredes laterales (-X y +X)
|
||||
append_box(buffers, -hw, -hw + th, th, h - th, -hd, hd);
|
||||
append_box(buffers, hw - th, hw, th, h - th, -hd, hd);
|
||||
|
||||
// 4. Pared frontal con vano para la puerta (+Z)
|
||||
append_box(buffers, -hw, -h_door_w, th, h - th, hd - th, hd);
|
||||
append_box(buffers, h_door_w, hw, th, h - th, hd - th, hd);
|
||||
append_box(buffers, -h_door_w, h_door_w, door_h, h - th, hd - th, hd);
|
||||
|
||||
// // 5. Botonera exterior e interior
|
||||
// append_box(buffers, h_door_w + 0.06, h_door_w + 0.18, 1.1, 1.35, hd - 0.01, hd + 0.03); // Pulsador fuera
|
||||
// append_box(buffers, hw - th - 0.02, hw - th, 1.1, 1.45, 0.1, 0.35); // Panel de botones interior
|
||||
|
||||
// 5. Botonera interior de la cabina (conservamos solo el panel de dentro)
|
||||
append_box(buffers, hw - th - 0.02, hw - th, 1.1, 1.45, 0.1, 0.35);
|
||||
|
||||
const geo = create_buffer_geometry(buffers);
|
||||
cache[key] = geo;
|
||||
return geo;
|
||||
};
|
||||
|
||||
this.get_door_leaf_geometry = () => {
|
||||
const key = `door_leaf`;
|
||||
if(cache[key]) return cache[key];
|
||||
|
||||
const buffers = { positions: [], normals: [], uvs: [], indices: [] };
|
||||
const leaf_w = 0.58;
|
||||
const leaf_h = 2.14;
|
||||
const leaf_th = 0.03;
|
||||
|
||||
append_box(buffers, -leaf_w * 0.5, leaf_w * 0.5, 0.02, leaf_h, -leaf_th * 0.5, leaf_th * 0.5);
|
||||
|
||||
const geo = create_buffer_geometry(buffers);
|
||||
cache[key] = geo;
|
||||
return geo;
|
||||
};
|
||||
|
||||
this.get_light_geometry = model => {
|
||||
const key = `light_panel`;
|
||||
if(cache[key]) return cache[key];
|
||||
|
||||
const buffers = { positions: [], normals: [], uvs: [], indices: [] };
|
||||
const h = model.height || hollow_bond.room_height || 3.0;
|
||||
|
||||
append_box(buffers, -0.4, 0.4, h - 0.1, h - 0.08, -0.4, 0.4);
|
||||
|
||||
const geo = create_buffer_geometry(buffers);
|
||||
cache[key] = geo;
|
||||
return geo;
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const k in cache){
|
||||
cache[k].dispose();
|
||||
delete cache[k];
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return ElevatorGeometryGame;
|
||||
})();
|
||||
178
Public/ecma/Geometries/Game/FrameGeometryGame.ecma.js
Normal file
178
Public/ecma/Geometries/Game/FrameGeometryGame.ecma.js
Normal file
@ -0,0 +1,178 @@
|
||||
"use strict";
|
||||
|
||||
const FrameGeometryGame = (function(){
|
||||
|
||||
const FrameGeometryGame = function(hollow_bond){
|
||||
|
||||
const cache = {};
|
||||
|
||||
/**
|
||||
* Construye el túnel del marco con moldura 3D sin solapamiento de suelo con la sala.
|
||||
*
|
||||
* @param {number} pass_w - Ancho libre del vano
|
||||
* @param {number} height - Altura libre del vano
|
||||
* @param {number} wall_depth - Grosor del tabique entre salas (room_padding * 2)
|
||||
* @param {boolean} is_horiz - true si corre en X, false si corre en Z
|
||||
*/
|
||||
this.get = (pass_w, height, wall_depth, is_horiz) => {
|
||||
|
||||
const key = `${pass_w}_${height}_${wall_depth}_${is_horiz}`;
|
||||
if(cache[key]){
|
||||
return cache[key];
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
|
||||
// Medias dimensiones del vano libre
|
||||
const hx = is_horiz ? pass_w / 2 : wall_depth / 2;
|
||||
const hz = is_horiz ? wall_depth / 2 : pass_w / 2;
|
||||
const hy = height / 2;
|
||||
|
||||
// Dimensiones de la moldura/tapajuntas
|
||||
const casing_w = 0.06; // 6 cm de solape sobre la pared
|
||||
const casing_t = 0.02; // 2 cm de relieve hacia el interior de la sala
|
||||
|
||||
const positions = [];
|
||||
const normals = [];
|
||||
const uvs = [];
|
||||
const indices = [];
|
||||
|
||||
let index_offset = 0;
|
||||
|
||||
const add_quad = (p1, p2, p3, p4, normal, uv_w, uv_h) => {
|
||||
positions.push(...p1, ...p2, ...p3, ...p4);
|
||||
normals.push(...normal, ...normal, ...normal, ...normal);
|
||||
uvs.push(0, 0, uv_w, 0, uv_w, uv_h, 0, uv_h);
|
||||
indices.push(
|
||||
index_offset, index_offset + 2, index_offset + 1,
|
||||
index_offset, index_offset + 3, index_offset + 2
|
||||
);
|
||||
index_offset += 4;
|
||||
};
|
||||
|
||||
if(is_horiz){
|
||||
const total_z = hz + casing_t;
|
||||
|
||||
// 1. UMBRAL / SUELO TRANSITABLE (+Y)
|
||||
// Se acota estrictamente a [-hz, hz] para que coincida justo con el final de la moqueta
|
||||
add_quad(
|
||||
[-hx, -hy, -hz], [ hx, -hy, -hz], [ hx, -hy, hz], [-hx, -hy, hz],
|
||||
[0, 1, 0], pass_w, wall_depth
|
||||
);
|
||||
|
||||
// 2. TECHO INTERIOR DEL VANO (-Y)
|
||||
// Aquí sí cubre hasta total_z para sellar la parte baja de la moldura superior
|
||||
add_quad(
|
||||
[-hx, hy, total_z], [ hx, hy, total_z], [ hx, hy, -total_z], [-hx, hy, -total_z],
|
||||
[0, -1, 0], pass_w, total_z * 2
|
||||
);
|
||||
|
||||
// 3. JAMBAS INTERIORES LATERALES
|
||||
// Izquierda (+X)
|
||||
add_quad(
|
||||
[-hx, -hy, -total_z], [-hx, -hy, total_z], [-hx, hy, total_z], [-hx, hy, -total_z],
|
||||
[1, 0, 0], total_z * 2, height
|
||||
);
|
||||
// Derecha (-X)
|
||||
add_quad(
|
||||
[ hx, -hy, total_z], [ hx, -hy, -total_z], [ hx, hy, -total_z], [ hx, hy, total_z],
|
||||
[-1, 0, 0], total_z * 2, height
|
||||
);
|
||||
|
||||
// 4. MOLDURAS EXTERIORES
|
||||
// Cara Sur (+Z)
|
||||
const z_s = total_z;
|
||||
add_quad([-hx - casing_w, -hy, z_s], [-hx, -hy, z_s], [-hx, hy + casing_w, z_s], [-hx - casing_w, hy + casing_w, z_s], [0, 0, 1], casing_w, height + casing_w);
|
||||
add_quad([ hx, -hy, z_s], [ hx + casing_w, -hy, z_s], [ hx + casing_w, hy + casing_w, z_s], [ hx, hy + casing_w, z_s], [0, 0, 1], casing_w, height + casing_w);
|
||||
add_quad([-hx, hy, z_s], [ hx, hy, z_s], [ hx, hy + casing_w, z_s], [-hx, hy + casing_w, z_s], [0, 0, 1], pass_w, casing_w);
|
||||
|
||||
// Cantos de cierre contra la pared (Sur)
|
||||
add_quad([-hx - casing_w, -hy, hz], [-hx - casing_w, -hy, z_s], [-hx - casing_w, hy + casing_w, z_s], [-hx - casing_w, hy + casing_w, hz], [-1, 0, 0], casing_t, height + casing_w);
|
||||
add_quad([ hx + casing_w, -hy, z_s], [ hx + casing_w, -hy, hz], [ hx + casing_w, hy + casing_w, hz], [ hx + casing_w, hy + casing_w, z_s], [1, 0, 0], casing_t, height + casing_w);
|
||||
add_quad([-hx - casing_w, hy + casing_w, hz], [ hx + casing_w, hy + casing_w, hz], [ hx + casing_w, hy + casing_w, z_s], [-hx - casing_w, hy + casing_w, z_s], [0, 1, 0], pass_w + casing_w * 2, casing_t);
|
||||
|
||||
// Cara Norte (-Z)
|
||||
const z_n = -total_z;
|
||||
add_quad([-hx, -hy, z_n], [-hx - casing_w, -hy, z_n], [-hx - casing_w, hy + casing_w, z_n], [-hx, hy + casing_w, z_n], [0, 0, -1], casing_w, height + casing_w);
|
||||
add_quad([ hx + casing_w, -hy, z_n], [ hx, -hy, z_n], [ hx, hy + casing_w, z_n], [ hx + casing_w, hy + casing_w, z_n], [0, 0, -1], casing_w, height + casing_w);
|
||||
add_quad([ hx, hy, z_n], [-hx, hy, z_n], [-hx, hy + casing_w, z_n], [ hx, hy + casing_w, z_n], [0, 0, -1], pass_w, casing_w);
|
||||
|
||||
// Cantos de cierre contra la pared (Norte)
|
||||
add_quad([-hx - casing_w, -hy, z_n], [-hx - casing_w, -hy, -hz], [-hx - casing_w, hy + casing_w, -hz], [-hx - casing_w, hy + casing_w, z_n], [-1, 0, 0], casing_t, height + casing_w);
|
||||
add_quad([ hx + casing_w, -hy, -hz], [ hx + casing_w, -hy, z_n], [ hx + casing_w, hy + casing_w, z_n], [ hx + casing_w, hy + casing_w, -hz], [1, 0, 0], casing_t, height + casing_w);
|
||||
add_quad([-hx - casing_w, hy + casing_w, z_n], [ hx + casing_w, hy + casing_w, z_n], [ hx + casing_w, hy + casing_w, -hz], [-hx - casing_w, hy + casing_w, -hz], [0, 1, 0], pass_w + casing_w * 2, casing_t);
|
||||
|
||||
} else {
|
||||
const total_x = hx + casing_t;
|
||||
|
||||
// 1. UMBRAL / SUELO TRANSITABLE (+Y)
|
||||
// Se acota a [-hx, hx] para alinearse de forma exacta a la moqueta
|
||||
add_quad(
|
||||
[-hx, -hy, -hz], [ hx, -hy, -hz], [ hx, -hy, hz], [-hx, -hy, hz],
|
||||
[0, 1, 0], wall_depth, pass_w
|
||||
);
|
||||
|
||||
// 2. TECHO INTERIOR (-Y)
|
||||
add_quad(
|
||||
[-total_x, hy, hz], [ total_x, hy, hz], [ total_x, hy, -hz], [-total_x, hy, -hz],
|
||||
[0, -1, 0], total_x * 2, pass_w
|
||||
);
|
||||
|
||||
// 3. JAMBAS INTERIORES LATERALES
|
||||
// Izquierda (+Z)
|
||||
add_quad(
|
||||
[-total_x, -hy, -hz], [ total_x, -hy, -hz], [ total_x, hy, -hz], [-total_x, hy, -hz],
|
||||
[0, 0, 1], total_x * 2, height
|
||||
);
|
||||
// Derecha (-Z)
|
||||
add_quad(
|
||||
[ total_x, -hy, hz], [-total_x, -hy, hz], [-total_x, hy, hz], [ total_x, hy, hz],
|
||||
[0, 0, -1], total_x * 2, height
|
||||
);
|
||||
|
||||
// 4. MOLDURAS EXTERIORES
|
||||
// Cara Este (+X)
|
||||
const x_e = total_x;
|
||||
add_quad([x_e, -hy, hz], [x_e, -hy, hz + casing_w], [x_e, hy + casing_w, hz + casing_w], [x_e, hy + casing_w, hz], [1, 0, 0], casing_w, height + casing_w);
|
||||
add_quad([x_e, -hy, -hz - casing_w], [x_e, -hy, -hz], [x_e, hy + casing_w, -hz], [x_e, hy + casing_w, -hz - casing_w], [1, 0, 0], casing_w, height + casing_w);
|
||||
add_quad([x_e, hy, -hz], [x_e, hy, hz], [x_e, hy + casing_w, hz], [x_e, hy + casing_w, -hz], [1, 0, 0], pass_w, casing_w);
|
||||
|
||||
// Cantos Este
|
||||
add_quad([hx, -hy, -hz - casing_w], [x_e, -hy, -hz - casing_w], [x_e, hy + casing_w, -hz - casing_w], [hx, hy + casing_w, -hz - casing_w], [0, 0, -1], casing_t, height + casing_w);
|
||||
add_quad([x_e, -hy, hz + casing_w], [hx, -hy, hz + casing_w], [hx, hy + casing_w, hz + casing_w], [x_e, hy + casing_w, hz + casing_w], [0, 0, 1], casing_t, height + casing_w);
|
||||
add_quad([hx, hy + casing_w, -hz - casing_w], [x_e, hy + casing_w, -hz - casing_w], [x_e, hy + casing_w, hz + casing_w], [hx, hy + casing_w, hz + casing_w], [0, 1, 0], casing_t, pass_w + casing_w * 2);
|
||||
|
||||
// Cara Oeste (-X)
|
||||
const x_w = -total_x;
|
||||
add_quad([x_w, -hy, hz + casing_w], [x_w, -hy, hz], [x_w, hy + casing_w, hz], [x_w, hy + casing_w, hz + casing_w], [-1, 0, 0], casing_w, height + casing_w);
|
||||
add_quad([x_w, -hy, -hz], [x_w, -hy, -hz - casing_w], [x_w, hy + casing_w, -hz - casing_w], [x_w, hy + casing_w, -hz], [-1, 0, 0], casing_w, height + casing_w);
|
||||
add_quad([x_w, hy, hz], [x_w, hy, -hz], [x_w, hy + casing_w, -hz], [x_w, hy + casing_w, hz], [-1, 0, 0], pass_w, casing_w);
|
||||
|
||||
// Cantos Oeste
|
||||
add_quad([x_w, -hy, -hz - casing_w], [-hx, -hy, -hz - casing_w], [-hx, hy + casing_w, -hz - casing_w], [x_w, hy + casing_w, -hz - casing_w], [0, 0, -1], casing_t, height + casing_w);
|
||||
add_quad([-hx, -hy, hz + casing_w], [x_w, -hy, hz + casing_w], [x_w, hy + casing_w, hz + casing_w], [-hx, hy + casing_w, hz + casing_w], [0, 0, 1], casing_t, height + casing_w);
|
||||
add_quad([x_w, hy + casing_w, -hz - casing_w], [-hx, hy + casing_w, -hz - casing_w], [-hx, hy + casing_w, hz + casing_w], [x_w, hy + casing_w, hz + casing_w], [0, 1, 0], casing_t, pass_w + casing_w * 2);
|
||||
}
|
||||
|
||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
||||
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
|
||||
geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
|
||||
geometry.setIndex(indices);
|
||||
|
||||
cache[key] = geometry;
|
||||
return geometry;
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const key in cache){
|
||||
if(cache[key]){
|
||||
cache[key].dispose();
|
||||
delete cache[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return FrameGeometryGame;
|
||||
})();
|
||||
118
Public/ecma/Geometries/Game/HoleGeometryGame.ecma.js
Normal file
118
Public/ecma/Geometries/Game/HoleGeometryGame.ecma.js
Normal file
@ -0,0 +1,118 @@
|
||||
"use strict";
|
||||
|
||||
const HoleGeometryGame = (function(){
|
||||
|
||||
const HoleGeometryGame = function(hollow_bond){
|
||||
|
||||
const cache = {};
|
||||
|
||||
/**
|
||||
* Genera las caras verticales del forjado que conectan los pisos intermedios.
|
||||
*
|
||||
* @param {number} width - Ancho en X (xt - xf)
|
||||
* @param {number} depth - Fondo en Z (zt - zf)
|
||||
* @param {number} yf - Piso inferior
|
||||
* @param {number} yt - Piso superior
|
||||
* @param {number} floor_h - Altura de piso (room_height)
|
||||
* @param {number} pad_y - Padding vertical de habitación
|
||||
*/
|
||||
this.get = (width, depth, yf, yt, floor_h, pad_y) => {
|
||||
|
||||
const slab_thick = pad_y * 2;
|
||||
const key = `${width}_${depth}_${yf}_${yt}_${slab_thick}`;
|
||||
|
||||
if(cache[key]){
|
||||
return cache[key];
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
|
||||
const hx = width / 2;
|
||||
const hz = depth / 2;
|
||||
|
||||
const positions = [];
|
||||
const normals = [];
|
||||
const uvs = [];
|
||||
const indices = [];
|
||||
|
||||
let index_offset = 0;
|
||||
|
||||
const add_quad = (p1, p2, p3, p4, normal, uv_w, uv_h) => {
|
||||
positions.push(...p1, ...p2, ...p3, ...p4);
|
||||
normals.push(...normal, ...normal, ...normal, ...normal);
|
||||
uvs.push(0, 0, uv_w, 0, uv_w, uv_h, 0, uv_h);
|
||||
indices.push(
|
||||
index_offset, index_offset + 2, index_offset + 1,
|
||||
index_offset, index_offset + 3, index_offset + 2
|
||||
);
|
||||
index_offset += 4;
|
||||
};
|
||||
|
||||
// Recorremos cada unión de forjado entre pisos
|
||||
for(let floor = yf; floor < yt; floor++){
|
||||
// Cota inferior: techo de la habitación inferior
|
||||
const y_bottom = ((floor + 1) * floor_h) - pad_y;
|
||||
// Cota superior: suelo de la habitación superior
|
||||
const y_top = ((floor + 1) * floor_h) + pad_y;
|
||||
|
||||
if(slab_thick > 0.001){
|
||||
// Pared Norte (Z = -hz, mirando hacia +Z)
|
||||
add_quad(
|
||||
[ hx, y_bottom, -hz],
|
||||
[-hx, y_bottom, -hz],
|
||||
[-hx, y_top, -hz],
|
||||
[ hx, y_top, -hz],
|
||||
[0, 0, 1], width, slab_thick
|
||||
);
|
||||
|
||||
// Pared Sur (Z = +hz, mirando hacia -Z)
|
||||
add_quad(
|
||||
[-hx, y_bottom, hz],
|
||||
[ hx, y_bottom, hz],
|
||||
[ hx, y_top, hz],
|
||||
[-hx, y_top, hz],
|
||||
[0, 0, -1], width, slab_thick
|
||||
);
|
||||
|
||||
// Pared Oeste (X = -hx, mirando hacia +X)
|
||||
add_quad(
|
||||
[-hx, y_bottom, hz],
|
||||
[-hx, y_bottom, -hz],
|
||||
[-hx, y_top, -hz],
|
||||
[-hx, y_top, hz],
|
||||
[1, 0, 0], depth, slab_thick
|
||||
);
|
||||
|
||||
// Pared Este (X = +hx, mirando hacia -X)
|
||||
add_quad(
|
||||
[ hx, y_bottom, -hz],
|
||||
[ hx, y_bottom, hz],
|
||||
[ hx, y_top, hz],
|
||||
[ hx, y_top, -hz],
|
||||
[-1, 0, 0], depth, slab_thick
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
||||
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
|
||||
geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
|
||||
geometry.setIndex(indices);
|
||||
|
||||
cache[key] = geometry;
|
||||
return geometry;
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const key in cache){
|
||||
if(cache[key]){
|
||||
cache[key].dispose();
|
||||
delete cache[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return HoleGeometryGame;
|
||||
})();
|
||||
92
Public/ecma/Geometries/Game/RailingGeometryGame.ecma.js
Normal file
92
Public/ecma/Geometries/Game/RailingGeometryGame.ecma.js
Normal file
@ -0,0 +1,92 @@
|
||||
"use strict";
|
||||
|
||||
const RailingGeometryGame = (function(){
|
||||
|
||||
const RailingGeometryGame = function(hollow_bond){
|
||||
|
||||
const cache = {};
|
||||
|
||||
// Genera un prisma cuadrangular (box) dentro de los buffers de la geometría
|
||||
const append_box = (buffers, min_x, max_x, min_y, max_y, min_z, max_z) => {
|
||||
const { positions, normals, uvs, indices } = buffers;
|
||||
let offset = positions.length / 3;
|
||||
|
||||
const add_quad = (p1, p2, p3, p4, n) => {
|
||||
positions.push(...p1, ...p2, ...p3, ...p4);
|
||||
normals.push(...n, ...n, ...n, ...n);
|
||||
uvs.push(0, 0, 1, 0, 1, 1, 0, 1);
|
||||
indices.push(offset, offset + 2, offset + 1, offset, offset + 3, offset + 2);
|
||||
offset += 4;
|
||||
};
|
||||
|
||||
// Top (+Y) y Bottom (-Y)
|
||||
add_quad([min_x, max_y, max_z], [max_x, max_y, max_z], [max_x, max_y, min_z], [min_x, max_y, min_z], [0, 1, 0]);
|
||||
add_quad([min_x, min_y, min_z], [max_x, min_y, min_z], [max_x, min_y, max_z], [min_x, min_y, max_z], [0, -1, 0]);
|
||||
|
||||
// North (-Z) y South (+Z)
|
||||
add_quad([max_x, min_y, min_z], [min_x, min_y, min_z], [min_x, max_y, min_z], [max_x, max_y, min_z], [0, 0, -1]);
|
||||
add_quad([min_x, min_y, max_z], [max_x, min_y, max_z], [max_x, max_y, max_z], [min_x, max_y, max_z], [0, 0, 1]);
|
||||
|
||||
// West (-X) y East (+X)
|
||||
add_quad([min_x, min_y, min_z], [min_x, min_y, max_z], [min_x, max_y, max_z], [min_x, max_y, min_z], [-1, 0, 0]);
|
||||
add_quad([max_x, min_y, max_z], [max_x, min_y, min_z], [max_x, max_y, min_z], [max_x, max_y, max_z], [1, 0, 0]);
|
||||
};
|
||||
|
||||
this.get_for_segment = segment => {
|
||||
const is_x = (segment.side === "north" || segment.side === "south");
|
||||
const length = is_x ? Math.abs(segment.xt - segment.xf) : Math.abs(segment.zt - segment.zf);
|
||||
const height = segment.height || 1.0;
|
||||
const th = segment.thickness || 0.08;
|
||||
const h_th = th * 0.5;
|
||||
|
||||
const key = `rail_${is_x ? "x" : "z"}_${length.toFixed(2)}`;
|
||||
if(cache[key]){
|
||||
return cache[key];
|
||||
}
|
||||
|
||||
const buffers = { positions: [], normals: [], uvs: [], indices: [] };
|
||||
const half_len = length * 0.5;
|
||||
|
||||
// 1. Pasamanos superior (1.0m) y Travesaño medio (0.5m)
|
||||
if(is_x){
|
||||
append_box(buffers, -half_len, half_len, height - 0.05, height, -h_th, h_th);
|
||||
append_box(buffers, -half_len, half_len, height * 0.5 - 0.02, height * 0.5 + 0.02, -h_th * 0.7, h_th * 0.7);
|
||||
} else {
|
||||
append_box(buffers, -h_th, h_th, height - 0.05, height, -half_len, half_len);
|
||||
append_box(buffers, -h_th * 0.7, h_th * 0.7, height * 0.5 - 0.02, height * 0.5 + 0.02, -half_len, half_len);
|
||||
}
|
||||
|
||||
// 2. Postes verticales (en los extremos y espaciados cada ~1.0m)
|
||||
const post_count = Math.max(2, Math.round(length / 1.0) + 1);
|
||||
for(let i = 0; i < post_count; i++){
|
||||
const t = i / (post_count - 1);
|
||||
const pos = (t - 0.5) * length;
|
||||
|
||||
if(is_x){
|
||||
append_box(buffers, pos - h_th, pos + h_th, 0, height, -h_th, h_th);
|
||||
} else {
|
||||
append_box(buffers, -h_th, h_th, 0, height, pos - h_th, pos + h_th);
|
||||
}
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(buffers.positions, 3));
|
||||
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(buffers.normals, 3));
|
||||
geometry.setAttribute("uv", new THREE.Float32BufferAttribute(buffers.uvs, 2));
|
||||
geometry.setIndex(buffers.indices);
|
||||
|
||||
cache[key] = geometry;
|
||||
return geometry;
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const k in cache){
|
||||
cache[k].dispose();
|
||||
delete cache[k];
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return RailingGeometryGame;
|
||||
})();
|
||||
270
Public/ecma/Geometries/Game/RoomGeometryGame.ecma.js
Normal file
270
Public/ecma/Geometries/Game/RoomGeometryGame.ecma.js
Normal file
@ -0,0 +1,270 @@
|
||||
"use strict";
|
||||
|
||||
const RoomGeometryGame = (function(){
|
||||
|
||||
const RoomGeometryGame = function(hollow_bond){
|
||||
|
||||
const cache = {};
|
||||
|
||||
/**
|
||||
* @param {RoomModel} model
|
||||
* @param {Array<FrameModel>} frames
|
||||
* @param {Array<HoleModel>} holes
|
||||
*/
|
||||
this.get_for_room = (model, frames = [], holes = []) => {
|
||||
|
||||
const pad = model.padding || 0;
|
||||
const w = Math.max(0.1, model.width - pad * 2);
|
||||
const h = Math.max(0.1, model.height - pad * 2);
|
||||
const d = Math.max(0.1, model.depth - pad * 2);
|
||||
|
||||
const frame_ids = frames.map(f => f.id).sort().join(",");
|
||||
const hole_ids = holes.map(h => h.id).sort().join(",");
|
||||
// const key = `${w}_${h}_${d}_[F:${frame_ids}]_[H:${hole_ids}]`;
|
||||
const key = model.id;
|
||||
|
||||
if(cache[key]){
|
||||
return cache[key];
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
|
||||
const hx = w / 2;
|
||||
const hy = h / 2;
|
||||
const hz = d / 2;
|
||||
|
||||
const positions = [];
|
||||
const normals = [];
|
||||
const uvs = [];
|
||||
const indices = [];
|
||||
|
||||
let index_offset = 0;
|
||||
|
||||
const add_quad = (p1, p2, p3, p4, normal, uv_w, uv_h, uv_ox = 0, uv_oy = 0) => {
|
||||
positions.push(...p1, ...p2, ...p3, ...p4);
|
||||
normals.push(...normal, ...normal, ...normal, ...normal);
|
||||
uvs.push(
|
||||
uv_ox, uv_oy,
|
||||
uv_ox + uv_w, uv_oy,
|
||||
uv_ox + uv_w, uv_oy + uv_h,
|
||||
uv_ox, uv_oy + uv_h
|
||||
);
|
||||
indices.push(
|
||||
index_offset, index_offset + 2, index_offset + 1,
|
||||
index_offset, index_offset + 3, index_offset + 2
|
||||
);
|
||||
index_offset += 4;
|
||||
};
|
||||
|
||||
// Obtenemos el índice del piso normalizado (0, 1, -1, -2...)
|
||||
const py = Math.round(model.y / model.height);
|
||||
|
||||
// Filtrado de huecos activos en Suelo y Techo
|
||||
// Suelo perforado solo si el pozo continúa hacia abajo
|
||||
const floor_holes = holes.filter(hole => py > hole.yf && py < hole.yt);
|
||||
|
||||
// Techo perforado solo si el pozo continúa hacia arriba
|
||||
const ceiling_holes = holes.filter(hole => py >= hole.yf && py < (hole.yt - 1));
|
||||
|
||||
// Subdivisión de un plano horizontal alrededor de 1 o más huecos
|
||||
const build_horizontal_plane = (hole_list, is_floor) => {
|
||||
const y_pos = is_floor ? -hy : hy;
|
||||
const norm = is_floor ? [0, 1, 0] : [0, -1, 0];
|
||||
|
||||
const add_surface_patch = (x1, x2, z1, z2) => {
|
||||
const pw = x2 - x1;
|
||||
const pd = z2 - z1;
|
||||
if(pw <= 0.001 || pd <= 0.001) return;
|
||||
|
||||
const px1 = -hx + x1, px2 = -hx + x2;
|
||||
const pz1 = -hz + z1, pz2 = -hz + z2;
|
||||
|
||||
if(is_floor){
|
||||
add_quad(
|
||||
[px1, y_pos, pz1],
|
||||
[px2, y_pos, pz1],
|
||||
[px2, y_pos, pz2],
|
||||
[px1, y_pos, pz2],
|
||||
norm, pw, pd, x1, z1
|
||||
);
|
||||
} else {
|
||||
add_quad(
|
||||
[px1, y_pos, pz2],
|
||||
[px2, y_pos, pz2],
|
||||
[px2, y_pos, pz1],
|
||||
[px1, y_pos, pz1],
|
||||
norm, pw, pd, x1, z1
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Si no hay huecos en este plano, generamos el plano entero de una pieza
|
||||
if(!hole_list || hole_list.length === 0){
|
||||
add_surface_patch(0, w, 0, d);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lista de parches de superficie sólida inicial (toda la habitación)
|
||||
let patches = [{ x1: 0, x2: w, z1: 0, z2: d }];
|
||||
|
||||
for(const target_hole of hole_list){
|
||||
const hx1 = Math.max(0, target_hole.xf - (model.x + pad));
|
||||
const hx2 = Math.min(w, target_hole.xt - (model.x + pad));
|
||||
const hz1 = Math.max(0, target_hole.zf - (model.z + pad));
|
||||
const hz2 = Math.min(d, target_hole.zt - (model.z + pad));
|
||||
|
||||
// Si el hueco no solapa con el espacio útil de la sala, lo ignoramos
|
||||
if(hx2 <= hx1 || hz2 <= hz1) continue;
|
||||
|
||||
const next_patches = [];
|
||||
|
||||
for(const p of patches){
|
||||
// Comprobar si el parche actual 'p' interseca con este hueco
|
||||
const ix1 = Math.max(p.x1, hx1);
|
||||
const ix2 = Math.min(p.x2, hx2);
|
||||
const iz1 = Math.max(p.z1, hz1);
|
||||
const iz2 = Math.min(p.z2, hz2);
|
||||
|
||||
// Si no hay solape, el parche se conserva íntegro
|
||||
if(ix1 >= ix2 || iz1 >= iz2){
|
||||
next_patches.push(p);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sustracción 2D: dividimos el parche en las 4 regiones que rodean el hueco
|
||||
// 1. Franja Norte (Z entre p.z1 e iz1)
|
||||
if(iz1 > p.z1){
|
||||
next_patches.push({ x1: p.x1, x2: p.x2, z1: p.z1, z2: iz1 });
|
||||
}
|
||||
// 2. Franja Sur (Z entre iz2 y p.z2)
|
||||
if(p.z2 > iz2){
|
||||
next_patches.push({ x1: p.x1, x2: p.x2, z1: iz2, z2: p.z2 });
|
||||
}
|
||||
// 3. Franja Oeste (X entre p.x1 e ix1, Z recortado entre iz1 e iz2)
|
||||
if(ix1 > p.x1){
|
||||
next_patches.push({ x1: p.x1, x2: ix1, z1: iz1, z2: iz2 });
|
||||
}
|
||||
// 4. Franja Este (X entre ix2 y p.x2, Z recortado entre iz1 e iz2)
|
||||
if(p.x2 > ix2){
|
||||
next_patches.push({ x1: ix2, x2: p.x2, z1: iz1, z2: iz2 });
|
||||
}
|
||||
}
|
||||
|
||||
patches = next_patches;
|
||||
}
|
||||
|
||||
// Generamos la geometría de todos los parches sólidos resultantes
|
||||
for(const p of patches){
|
||||
add_surface_patch(p.x1, p.x2, p.z1, p.z2);
|
||||
}
|
||||
};
|
||||
|
||||
// --- 1. SUELO (+Y) -> Grupo 0 ---
|
||||
const floor_start = indices.length;
|
||||
build_horizontal_plane(floor_holes, true);
|
||||
geometry.addGroup(floor_start, indices.length - floor_start, 0);
|
||||
|
||||
// --- 2. TECHO (-Y) -> Grupo 1 ---
|
||||
const ceiling_start = indices.length;
|
||||
build_horizontal_plane(ceiling_holes, false);
|
||||
geometry.addGroup(ceiling_start, indices.length - ceiling_start, 1);
|
||||
|
||||
// --- 3. PAREDES -> Grupo 2 ---
|
||||
const walls_start = indices.length;
|
||||
|
||||
const wall_frames = { north: [], south: [], west: [], east: [] };
|
||||
frames.forEach(f => {
|
||||
if(f.axis === "horizontal"){
|
||||
const dist_north = Math.abs(f.z - model.z);
|
||||
const dist_south = Math.abs(f.z - (model.z + model.depth));
|
||||
if(dist_north <= dist_south) wall_frames.north.push(f);
|
||||
else wall_frames.south.push(f);
|
||||
} else {
|
||||
const dist_west = Math.abs(f.x - model.x);
|
||||
const dist_east = Math.abs(f.x - (model.x + model.width));
|
||||
if(dist_west <= dist_east) wall_frames.west.push(f);
|
||||
else wall_frames.east.push(f);
|
||||
}
|
||||
});
|
||||
|
||||
const build_segmented_wall = (wall_len, wall_h, frame_list, add_rect, normal) => {
|
||||
if(!frame_list || frame_list.length === 0){
|
||||
add_rect(0, wall_len, 0, wall_h, normal);
|
||||
return;
|
||||
}
|
||||
|
||||
const openings = frame_list.map(f => {
|
||||
const offset = (f.axis === "horizontal")
|
||||
? (f.x - (model.x + pad))
|
||||
: (f.z - (model.z + pad));
|
||||
return {
|
||||
start: Math.max(0, offset),
|
||||
end: Math.min(wall_len, offset + f.width),
|
||||
height: Math.min(wall_h, f.height)
|
||||
};
|
||||
}).sort((a, b) => a.start - b.start);
|
||||
|
||||
let current_x = 0;
|
||||
openings.forEach(op => {
|
||||
if(op.start > current_x){
|
||||
add_rect(current_x, op.start, 0, wall_h, normal);
|
||||
}
|
||||
if(op.height < wall_h){
|
||||
add_rect(op.start, op.end, op.height, wall_h, normal);
|
||||
}
|
||||
current_x = Math.max(current_x, op.end);
|
||||
});
|
||||
|
||||
if(current_x < wall_len){
|
||||
add_rect(current_x, wall_len, 0, wall_h, normal);
|
||||
}
|
||||
};
|
||||
|
||||
// Norte (-Z)
|
||||
build_segmented_wall(w, h, wall_frames.north, (x1, x2, y1, y2, norm) => {
|
||||
const px1 = -hx + x1, px2 = -hx + x2, py1 = -hy + y1, py2 = -hy + y2;
|
||||
add_quad([px2, py1, -hz], [px1, py1, -hz], [px1, py2, -hz], [px2, py2, -hz], norm, x2 - x1, y2 - y1, x1, y1);
|
||||
}, [0, 0, 1]);
|
||||
|
||||
// Sur (+Z)
|
||||
build_segmented_wall(w, h, wall_frames.south, (x1, x2, y1, y2, norm) => {
|
||||
const px1 = -hx + x1, px2 = -hx + x2, py1 = -hy + y1, py2 = -hy + y2;
|
||||
add_quad([px1, py1, hz], [px2, py1, hz], [px2, py2, hz], [px1, py2, hz], norm, x2 - x1, y2 - y1, x1, y1);
|
||||
}, [0, 0, -1]);
|
||||
|
||||
// Oeste (-X)
|
||||
build_segmented_wall(d, h, wall_frames.west, (z1, z2, y1, y2, norm) => {
|
||||
const pz1 = -hz + z1, pz2 = -hz + z2, py1 = -hy + y1, py2 = -hy + y2;
|
||||
add_quad([-hx, py1, pz1], [-hx, py1, pz2], [-hx, py2, pz2], [-hx, py2, pz1], norm, z2 - z1, y2 - y1, z1, y1);
|
||||
}, [1, 0, 0]);
|
||||
|
||||
// Este (+X)
|
||||
build_segmented_wall(d, h, wall_frames.east, (z1, z2, y1, y2, norm) => {
|
||||
const pz1 = -hz + z1, pz2 = -hz + z2, py1 = -hy + y1, py2 = -hy + y2;
|
||||
add_quad([hx, py1, pz2], [hx, py1, pz1], [hx, py2, pz1], [hx, py2, pz2], norm, z2 - z1, y2 - y1, z1, y1);
|
||||
}, [-1, 0, 0]);
|
||||
|
||||
geometry.addGroup(walls_start, indices.length - walls_start, 2);
|
||||
|
||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
||||
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
|
||||
geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
|
||||
geometry.setIndex(indices);
|
||||
|
||||
cache[key] = geometry;
|
||||
return geometry;
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const key in cache){
|
||||
if(cache[key]){
|
||||
cache[key].dispose();
|
||||
delete cache[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return RoomGeometryGame;
|
||||
})();
|
||||
256
Public/ecma/Geometries/Game/StairGeometryGame.ecma.js
Normal file
256
Public/ecma/Geometries/Game/StairGeometryGame.ecma.js
Normal file
@ -0,0 +1,256 @@
|
||||
"use strict";
|
||||
|
||||
const StairGeometryGame = (function(){
|
||||
|
||||
const StairGeometryGame = function(hollow_bond){
|
||||
|
||||
const cache = {};
|
||||
|
||||
// Utilidad para añadir un prisma ortogonal (caja AABB)
|
||||
const append_box = (buffers, min_x, max_x, min_y, max_y, min_z, max_z) => {
|
||||
const { positions, normals, uvs, indices } = buffers;
|
||||
let offset = positions.length / 3;
|
||||
|
||||
const add_quad = (p1, p2, p3, p4, n) => {
|
||||
positions.push(...p1, ...p2, ...p3, ...p4);
|
||||
normals.push(...n, ...n, ...n, ...n);
|
||||
uvs.push(0, 0, 1, 0, 1, 1, 0, 1);
|
||||
indices.push(offset, offset + 2, offset + 1, offset, offset + 3, offset + 2);
|
||||
offset += 4;
|
||||
};
|
||||
|
||||
add_quad([min_x, max_y, max_z], [max_x, max_y, max_z], [max_x, max_y, min_z], [min_x, max_y, min_z], [0, 1, 0]);
|
||||
add_quad([min_x, min_y, min_z], [max_x, min_y, min_z], [max_x, min_y, max_z], [min_x, min_y, max_z], [0, -1, 0]);
|
||||
add_quad([max_x, min_y, min_z], [min_x, min_y, min_z], [min_x, max_y, min_z], [max_x, max_y, min_z], [0, 0, -1]);
|
||||
add_quad([min_x, min_y, max_z], [max_x, min_y, max_z], [max_x, max_y, max_z], [min_x, max_y, max_z], [0, 0, 1]);
|
||||
add_quad([min_x, min_y, min_z], [min_x, min_y, max_z], [min_x, max_y, max_z], [min_x, max_y, min_z], [-1, 0, 0]);
|
||||
add_quad([max_x, min_y, max_z], [max_x, min_y, min_z], [max_x, max_y, min_z], [max_x, max_y, max_z], [1, 0, 0]);
|
||||
};
|
||||
|
||||
// Genera una barra inclinada continua conectando (L0, Y0) con (L1, Y1)
|
||||
const append_inclined_bar = (buffers, is_x, l0, y0, l1, y1, c_min, c_max, bar_h) => {
|
||||
const { positions, normals, uvs, indices } = buffers;
|
||||
let offset = positions.length / 3;
|
||||
|
||||
const add_quad = (p1, p2, p3, p4, n) => {
|
||||
positions.push(...p1, ...p2, ...p3, ...p4);
|
||||
normals.push(...n, ...n, ...n, ...n);
|
||||
uvs.push(0, 0, 1, 0, 1, 1, 0, 1);
|
||||
indices.push(offset, offset + 2, offset + 1, offset, offset + 3, offset + 2);
|
||||
offset += 4;
|
||||
};
|
||||
|
||||
const make_pt = (l, y, c) => is_x ? [l, y, c] : [c, y, l];
|
||||
|
||||
const p0 = make_pt(l0, y0, c_min);
|
||||
const p1 = make_pt(l0, y0, c_max);
|
||||
const p2 = make_pt(l0, y0 + bar_h, c_max);
|
||||
const p3 = make_pt(l0, y0 + bar_h, c_min);
|
||||
|
||||
const p4 = make_pt(l1, y1, c_min);
|
||||
const p5 = make_pt(l1, y1, c_max);
|
||||
const p6 = make_pt(l1, y1 + bar_h, c_max);
|
||||
const p7 = make_pt(l1, y1 + bar_h, c_min);
|
||||
|
||||
// Caras longitudinales
|
||||
add_quad(p3, p7, p6, p2, [0, 1, 0]); // Superior
|
||||
add_quad(p0, p1, p5, p4, [0, -1, 0]); // Inferior
|
||||
add_quad(p0, p4, p7, p3, is_x ? [0, 0, -1] : [-1, 0, 0]); // Lateral C_min
|
||||
add_quad(p1, p2, p6, p5, is_x ? [0, 0, 1] : [1, 0, 0]); // Lateral C_max
|
||||
|
||||
// Tapas en los extremos
|
||||
add_quad(p0, p3, p2, p1, is_x ? [-1, 0, 0] : [0, 0, -1]);
|
||||
add_quad(p4, p5, p6, p7, is_x ? [1, 0, 0] : [0, 0, 1]);
|
||||
};
|
||||
|
||||
this.get_for_stair = (stair, hole) => {
|
||||
const is_x = (stair.wall === "north" || stair.wall === "south");
|
||||
const length = is_x ? stair.width : stair.depth;
|
||||
const rise_height = hollow_bond.room_height;
|
||||
const landing_size = hollow_bond.stairs_landing_size || 1.0;
|
||||
|
||||
const key = `stair_v2_${stair.wall}_dir${stair.direction}_len${length}_h${rise_height}`;
|
||||
if(cache[key]) return cache[key];
|
||||
|
||||
const buffers = { positions: [], normals: [], uvs: [], indices: [] };
|
||||
|
||||
const half_w = 0.5;
|
||||
const flight_len = Math.max(1.0, length - (landing_size * 2));
|
||||
const dir = stair.direction; // 1 o -1
|
||||
|
||||
// Orientación: qué lado da al vacío del foso
|
||||
const inner_sign = (stair.wall === "north" || stair.wall === "west") ? 1 : -1;
|
||||
const inner_c = inner_sign * (half_w - 0.04);
|
||||
const outer_c = -inner_sign * (half_w - 0.04);
|
||||
|
||||
// 1. DESCANGILLOS (Plataformas horizontales de 1m)
|
||||
const bot_l_start = (dir === 1) ? 0 : (length - landing_size);
|
||||
const top_l_start = (dir === 1) ? (length - landing_size) : 0;
|
||||
|
||||
const add_platform = (l_start, y_level) => {
|
||||
const l_end = l_start + landing_size;
|
||||
if(is_x){
|
||||
append_box(buffers, l_start, l_end, y_level - 0.05, y_level, -half_w, half_w);
|
||||
} else {
|
||||
append_box(buffers, -half_w, half_w, y_level - 0.05, y_level, l_start, l_end);
|
||||
}
|
||||
};
|
||||
|
||||
add_platform(bot_l_start, 0);
|
||||
add_platform(top_l_start, rise_height);
|
||||
|
||||
// 2. PELDAÑOS NUMEROSOS (~0.18m contrahuella)
|
||||
const step_count = Math.max(12, Math.round(rise_height / 0.1875));
|
||||
const step_rise = rise_height / step_count;
|
||||
const step_run = flight_len / step_count;
|
||||
|
||||
for(let i = 0; i < step_count; i++){
|
||||
let l_min, l_max;
|
||||
if(dir === 1){
|
||||
l_min = landing_size + (i * step_run);
|
||||
l_max = l_min + step_run;
|
||||
} else {
|
||||
l_max = (length - landing_size) - (i * step_run);
|
||||
l_min = l_max - step_run;
|
||||
}
|
||||
|
||||
const y_top = (i + 1) * step_rise;
|
||||
const y_bot = y_top - 0.035;
|
||||
|
||||
if(is_x){
|
||||
append_box(buffers, l_min, l_max, y_bot, y_top, -half_w + 0.06, half_w - 0.06);
|
||||
} else {
|
||||
append_box(buffers, -half_w + 0.06, half_w - 0.06, y_bot, y_top, l_min, l_max);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. ZANCAS / BARRAS INCLINADAS QUE UNEN LOS PELDAÑOS
|
||||
const flight_l_bot = (dir === 1) ? landing_size : (length - landing_size);
|
||||
const flight_l_top = (dir === 1) ? (length - landing_size) : landing_size;
|
||||
|
||||
// Zanca exterior (lado pared)
|
||||
append_inclined_bar(
|
||||
buffers, is_x,
|
||||
flight_l_bot, 0,
|
||||
flight_l_top, rise_height,
|
||||
outer_c - 0.025, outer_c + 0.025,
|
||||
0.07
|
||||
);
|
||||
|
||||
// Zanca interior (lado foso)
|
||||
append_inclined_bar(
|
||||
buffers, is_x,
|
||||
flight_l_bot, 0,
|
||||
flight_l_top, rise_height,
|
||||
inner_c - 0.025, inner_c + 0.025,
|
||||
0.07
|
||||
);
|
||||
|
||||
// 4. BARANDILLA INCLINADA Y POSTES (1.0m de altura, solo lado vacío)
|
||||
const rail_h = 1.0;
|
||||
|
||||
// Pasamanos superior (1.0m)
|
||||
append_inclined_bar(
|
||||
buffers, is_x,
|
||||
flight_l_bot, rail_h - 0.04,
|
||||
flight_l_top, rise_height + rail_h - 0.04,
|
||||
inner_c - 0.02, inner_c + 0.02,
|
||||
0.04
|
||||
);
|
||||
|
||||
// Travesaño intermedio (0.5m)
|
||||
append_inclined_bar(
|
||||
buffers, is_x,
|
||||
flight_l_bot, rail_h * 0.5 - 0.02,
|
||||
flight_l_top, rise_height + rail_h * 0.5 - 0.02,
|
||||
inner_c - 0.015, inner_c + 0.015,
|
||||
0.03
|
||||
);
|
||||
|
||||
// Postes verticales que nacen de la zanca y van al pasamanos
|
||||
const post_count = Math.max(4, Math.round(flight_len / 0.75) + 1);
|
||||
for(let k = 0; k < post_count; k++){
|
||||
const t = k / (post_count - 1);
|
||||
const l_post = (dir === 1)
|
||||
? (landing_size + t * flight_len)
|
||||
: ((length - landing_size) - t * flight_len);
|
||||
const y_base = t * rise_height;
|
||||
|
||||
if(is_x){
|
||||
append_box(buffers, l_post - 0.02, l_post + 0.02, y_base, y_base + rail_h, inner_c - 0.02, inner_c + 0.02);
|
||||
} else {
|
||||
append_box(buffers, inner_c - 0.02, inner_c + 0.02, y_base, y_base + rail_h, l_post - 0.02, l_post + 0.02);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. BARANDILLAS EN LOS DESCANSOS (Lado interior)
|
||||
const add_landing_rail = (l_start, y_level) => {
|
||||
const l_end = l_start + landing_size;
|
||||
// Pasamanos y travesaño
|
||||
if(is_x){
|
||||
append_box(buffers, l_start, l_end, y_level + rail_h - 0.04, y_level + rail_h, inner_c - 0.02, inner_c + 0.02);
|
||||
append_box(buffers, l_start, l_end, y_level + rail_h * 0.5 - 0.02, y_level + rail_h * 0.5 + 0.01, inner_c - 0.015, inner_c + 0.015);
|
||||
append_box(buffers, l_start - 0.02, l_start + 0.02, y_level, y_level + rail_h, inner_c - 0.02, inner_c + 0.02);
|
||||
append_box(buffers, l_end - 0.02, l_end + 0.02, y_level, y_level + rail_h, inner_c - 0.02, inner_c + 0.02);
|
||||
} else {
|
||||
append_box(buffers, inner_c - 0.02, inner_c + 0.02, y_level + rail_h - 0.04, y_level + rail_h, l_start, l_end);
|
||||
append_box(buffers, inner_c - 0.015, inner_c + 0.015, y_level + rail_h * 0.5 - 0.02, y_level + rail_h * 0.5 + 0.01, l_start, l_end);
|
||||
append_box(buffers, inner_c - 0.02, inner_c + 0.02, y_level, y_level + rail_h, l_start - 0.02, l_start + 0.02);
|
||||
append_box(buffers, inner_c - 0.02, inner_c + 0.02, y_level, y_level + rail_h, l_end - 0.02, l_end + 0.02);
|
||||
}
|
||||
};
|
||||
|
||||
add_landing_rail(bot_l_start, 0);
|
||||
add_landing_rail(top_l_start, rise_height);
|
||||
|
||||
// 6. CIERRE FRONTAL (si el descansillo da al vacío y no a una pared del hueco)
|
||||
if(hole){
|
||||
const at_corner_start = is_x ? (stair.xf === hole.xf) : (stair.zf === hole.zf);
|
||||
const at_corner_end = is_x ? (stair.xt === hole.xt) : (stair.zt === hole.zt);
|
||||
|
||||
// Cierre en extremo 0 (L = 0)
|
||||
if(!at_corner_start){
|
||||
const y_base = (dir === 1) ? 0 : rise_height;
|
||||
if(is_x){
|
||||
append_box(buffers, -0.02, 0.02, y_base + rail_h - 0.04, y_base + rail_h, -half_w, half_w);
|
||||
append_box(buffers, -0.02, 0.02, y_base, y_base + rail_h, outer_c - 0.02, outer_c + 0.02);
|
||||
} else {
|
||||
append_box(buffers, -half_w, half_w, y_base + rail_h - 0.04, y_base + rail_h, -0.02, 0.02);
|
||||
append_box(buffers, outer_c - 0.02, outer_c + 0.02, y_base, y_base + rail_h, -0.02, 0.02);
|
||||
}
|
||||
}
|
||||
|
||||
// Cierre en extremo length (L = length)
|
||||
if(!at_corner_end){
|
||||
const y_base = (dir === 1) ? rise_height : 0;
|
||||
if(is_x){
|
||||
append_box(buffers, length - 0.02, length + 0.02, y_base + rail_h - 0.04, y_base + rail_h, -half_w, half_w);
|
||||
append_box(buffers, length - 0.02, length + 0.02, y_base, y_base + rail_h, outer_c - 0.02, outer_c + 0.02);
|
||||
} else {
|
||||
append_box(buffers, -half_w, half_w, y_base + rail_h - 0.04, y_base + rail_h, length - 0.02, length + 0.02);
|
||||
append_box(buffers, outer_c - 0.02, outer_c + 0.02, y_base, y_base + rail_h, length - 0.02, length + 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(buffers.positions, 3));
|
||||
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(buffers.normals, 3));
|
||||
geometry.setAttribute("uv", new THREE.Float32BufferAttribute(buffers.uvs, 2));
|
||||
geometry.setIndex(buffers.indices);
|
||||
|
||||
cache[key] = geometry;
|
||||
return geometry;
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const k in cache){
|
||||
cache[k].dispose();
|
||||
delete cache[k];
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return StairGeometryGame;
|
||||
})();
|
||||
704
Public/ecma/Managers/DungeonManager.ecma.js
Normal file
704
Public/ecma/Managers/DungeonManager.ecma.js
Normal file
@ -0,0 +1,704 @@
|
||||
"use strict";
|
||||
|
||||
const DungeonManager = (function(){
|
||||
|
||||
const DungeonManager = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
let debug_hud = null; // <-- Referencia al HUD
|
||||
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
this.renderer = null;
|
||||
this.on = false;
|
||||
|
||||
this.floor_height = hollow_bond.room_height || 3;
|
||||
|
||||
// Variables de streaming por topología
|
||||
this.current_room = null;
|
||||
this.current_chunk = { x: 0, y: 0, z: 0 };
|
||||
|
||||
// Parámetros de niveles de salas (con fallback a 3 y 5)
|
||||
hollow_bond.horizontal_loaded_rooms_levels = (hollow_bond.horizontal_loaded_rooms_levels !== undefined)
|
||||
? hollow_bond.horizontal_loaded_rooms_levels
|
||||
: 3;
|
||||
|
||||
hollow_bond.horizontal_unloaded_rooms_level = (hollow_bond.horizontal_unloaded_rooms_level !== undefined)
|
||||
? hollow_bond.horizontal_unloaded_rooms_level
|
||||
: 5;
|
||||
|
||||
// Gestores de geometrías y materiales
|
||||
this.geometries = {
|
||||
room : (typeof RoomGeometryGame !== "undefined") ? new RoomGeometryGame(hollow_bond) : null,
|
||||
frame : (typeof FrameGeometryGame !== "undefined") ? new FrameGeometryGame(hollow_bond) : null,
|
||||
hole : (typeof HoleGeometryGame !== "undefined") ? new HoleGeometryGame(hollow_bond) : null,
|
||||
stair : (typeof StairGeometryGame !== "undefined") ? new StairGeometryGame(hollow_bond) : null,
|
||||
elevator : (typeof ElevatorGeometryGame !== "undefined") ? new ElevatorGeometryGame(hollow_bond) : null,
|
||||
railing : (typeof RailingGeometryGame !== "undefined") ? new RailingGeometryGame(hollow_bond) : null,
|
||||
elevator_frame : (typeof ElevatorFrameGeometryGame !== "undefined") ? new ElevatorFrameGeometryGame(hollow_bond) : null
|
||||
};
|
||||
|
||||
this.materials = {
|
||||
room : (typeof RoomMaterialGame !== "undefined") ? new RoomMaterialGame(hollow_bond) : null,
|
||||
frame : (typeof FrameMaterialGame !== "undefined") ? new FrameMaterialGame(hollow_bond) : null,
|
||||
hole : (typeof HoleMaterialGame !== "undefined") ? new HoleMaterialGame(hollow_bond) : null,
|
||||
stair : (typeof StairMaterialGame !== "undefined") ? new StairMaterialGame(hollow_bond) : null,
|
||||
elevator : (typeof ElevatorMaterialGame !== "undefined") ? new ElevatorMaterialGame(hollow_bond) : null,
|
||||
railing : (typeof RailingMaterialGame !== "undefined") ? new RailingMaterialGame(hollow_bond) : null,
|
||||
};
|
||||
|
||||
// Diccionarios de objetos visuales actualmente instanciados en escena
|
||||
this.objects = {
|
||||
room : {},
|
||||
frame : {},
|
||||
hole : {},
|
||||
stair : {},
|
||||
elevator : {},
|
||||
railing : {}
|
||||
};
|
||||
|
||||
let last_frame_time = performance.now();
|
||||
let animation_frame_id = null;
|
||||
|
||||
/**
|
||||
* Inicializa la partida en primera persona con la altura seleccionada.
|
||||
* @param {number} player_height
|
||||
*/
|
||||
this.start = (player_height = 1.75) => {
|
||||
self.on = true;
|
||||
setup_scene();
|
||||
|
||||
debug_hud = new DungeonHudView(hollow_bond, self);
|
||||
|
||||
if(hollow_bond.controls && hollow_bond.controls.game){
|
||||
hollow_bond.controls.game.set_dimensions(player_height);
|
||||
hollow_bond.controls.game.setup(self);
|
||||
}
|
||||
|
||||
self.current_chunk = { x: 0, y: 0, z: 0 };
|
||||
|
||||
// Generamos el chunk inicial y sus vecinos en MapManager
|
||||
hollow_bond.map.update_active_center(0, 0, 0, () => {
|
||||
let spawn_room_model = null;
|
||||
const center_chunk = hollow_bond.map.get_chunk(0, 0, 0);
|
||||
|
||||
if(center_chunk && center_chunk.rooms){
|
||||
for(const id in center_chunk.rooms){
|
||||
const r = center_chunk.rooms[id];
|
||||
// Priorizamos sala segura sin agujeros de caída
|
||||
if((!r.holes || r.holes.length === 0) && (!r.voids || r.voids.length === 0)){
|
||||
spawn_room_model = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!spawn_room_model){
|
||||
const keys = Object.keys(center_chunk.rooms);
|
||||
if(keys.length > 0) spawn_room_model = center_chunk.rooms[keys[0]];
|
||||
}
|
||||
}
|
||||
|
||||
if(spawn_room_model){
|
||||
// 1. Instanciamos la sala inicial de aparición
|
||||
const room_obj = new RoomObjectGame(
|
||||
hollow_bond,
|
||||
spawn_room_model,
|
||||
self.geometries.room,
|
||||
self.materials.room
|
||||
);
|
||||
self.objects.room[spawn_room_model.id] = room_obj;
|
||||
self.scene.add(room_obj.mesh);
|
||||
self.current_room = room_obj;
|
||||
|
||||
// 2. Ejecutamos el streaming para cargar las adyacentes hasta nivel 3
|
||||
self.update_rooms_streaming();
|
||||
|
||||
// 3. Ubicamos al jugador en el centro de la sala
|
||||
const spawn_x = spawn_room_model.x + spawn_room_model.width / 2;
|
||||
const spawn_z = spawn_room_model.z + spawn_room_model.depth / 2;
|
||||
const floor_y = room_obj.collider.get_floor_height();
|
||||
const eye_height = hollow_bond.controls.game.standing_height;
|
||||
|
||||
self.camera.position.set(spawn_x, floor_y + eye_height, spawn_z);
|
||||
}
|
||||
|
||||
if(hollow_bond.show_fps && hollow_bond.views && hollow_bond.views.fps){
|
||||
hollow_bond.views.fps.go();
|
||||
}
|
||||
|
||||
last_frame_time = performance.now();
|
||||
render_loop();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Comprueba en qué sala cargada se encuentra el jugador.
|
||||
*/
|
||||
const check_room_transition = () => {
|
||||
if(!self.current_room || !self.camera) return;
|
||||
|
||||
const pos = self.camera.position;
|
||||
const eye_height = (hollow_bond.controls && hollow_bond.controls.game)
|
||||
? hollow_bond.controls.game.current_height
|
||||
: 1.75;
|
||||
const feet_y = pos.y - eye_height;
|
||||
|
||||
// Si sigue dentro de la sala activa en 3D, no hacemos nada
|
||||
if(self.current_room.collider.contains(pos.x, feet_y, pos.z)){
|
||||
return;
|
||||
}
|
||||
|
||||
// Buscamos sala candidata filtrando estrictamente por cota vertical primero
|
||||
for(const rid in self.objects.room){
|
||||
const room_obj = self.objects.room[rid];
|
||||
if(room_obj === self.current_room) continue;
|
||||
|
||||
const room_floor_y = room_obj.collider.get_floor_height();
|
||||
|
||||
// Si la sala está en otro piso (a más de 1 metro de distancia vertical de los pies), SE IGNORA
|
||||
if(Math.abs(feet_y - room_floor_y) > 1.0) continue;
|
||||
|
||||
if(room_obj.collider.contains(pos.x, feet_y, pos.z)){
|
||||
self.set_current_room(room_obj);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Asigna la nueva sala activa y actualiza el streaming y los Chunks si corresponde.
|
||||
* @param {RoomObjectGame} room_obj
|
||||
*/
|
||||
this.set_current_room = room_obj => {
|
||||
if(!room_obj || self.current_room === room_obj) return;
|
||||
|
||||
self.current_room = room_obj;
|
||||
|
||||
const chunk_size = hollow_bond.chunk_size || 16;
|
||||
const chunk_height = (hollow_bond.chunk_height || (chunk_size * hollow_bond.room_height));
|
||||
|
||||
const cx = Math.floor((room_obj.model.x + room_obj.model.width / 2) / chunk_size);
|
||||
const cz = Math.floor((room_obj.model.z + room_obj.model.depth / 2) / chunk_size);
|
||||
const cy = Math.floor((room_obj.model.y + room_obj.model.height / 2) / chunk_height);
|
||||
|
||||
if(cx !== self.current_chunk.x || cy !== self.current_chunk.y || cz !== self.current_chunk.z){
|
||||
self.current_chunk = { x: cx, y: cy, z: cz };
|
||||
if(hollow_bond.map && hollow_bond.map.update_active_center){
|
||||
hollow_bond.map.update_active_center(cx, cy, cz, () => {
|
||||
self.update_rooms_streaming();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.update_rooms_streaming();
|
||||
};
|
||||
|
||||
/**
|
||||
* Algoritmo BFS para streaming de habitaciones y marcos por niveles de conectividad.
|
||||
*/
|
||||
this.update_rooms_streaming = () => {
|
||||
if(!self.current_room || !hollow_bond.map) return;
|
||||
|
||||
const load_limit = hollow_bond.horizontal_loaded_rooms_levels || 3;
|
||||
const unload_limit = hollow_bond.horizontal_unloaded_rooms_level || 5;
|
||||
|
||||
const queue = [{ id: self.current_room.model.id, depth: 0 }];
|
||||
const room_depths = { [self.current_room.model.id]: 0 };
|
||||
|
||||
while(queue.length > 0){
|
||||
const { id, depth } = queue.shift();
|
||||
|
||||
const room_model = hollow_bond.map.rooms ? hollow_bond.map.rooms[id] : null;
|
||||
if(!room_model) continue;
|
||||
|
||||
// A) Expansión horizontal por MARCOS
|
||||
if(room_model.frames){
|
||||
for(const fid of room_model.frames){
|
||||
const frame_model = hollow_bond.map.frames ? hollow_bond.map.frames[fid] : null;
|
||||
if(!frame_model || !frame_model.rooms) continue;
|
||||
|
||||
for(const neighbor_id of frame_model.rooms){
|
||||
if(neighbor_id && neighbor_id !== id && room_depths[neighbor_id] === undefined){
|
||||
const next_depth = depth + 1;
|
||||
room_depths[neighbor_id] = next_depth;
|
||||
if(next_depth <= unload_limit) queue.push({ id: neighbor_id, depth: next_depth });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// B) Expansión vertical por HUECOS
|
||||
if(room_model.holes){
|
||||
for(const hid of room_model.holes){
|
||||
const hole_model = hollow_bond.map.holes ? hollow_bond.map.holes[hid] : null;
|
||||
if(!hole_model || !hole_model.rooms) continue;
|
||||
|
||||
for(const neighbor_id of hole_model.rooms){
|
||||
if(neighbor_id && neighbor_id !== id && room_depths[neighbor_id] === undefined){
|
||||
const next_depth = depth + 1;
|
||||
room_depths[neighbor_id] = next_depth;
|
||||
if(next_depth <= unload_limit) queue.push({ id: neighbor_id, depth: next_depth });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// C) Expansión vertical por ASCENSORES
|
||||
if(room_model.elevators){
|
||||
for(const eid of room_model.elevators){
|
||||
const elev_model = hollow_bond.map.elevators ? hollow_bond.map.elevators[eid] : null;
|
||||
if(!elev_model || !elev_model.rooms) continue;
|
||||
|
||||
for(const neighbor_id of elev_model.rooms){
|
||||
if(neighbor_id && neighbor_id !== id && room_depths[neighbor_id] === undefined){
|
||||
const next_depth = depth + 1;
|
||||
room_depths[neighbor_id] = next_depth;
|
||||
if(next_depth <= unload_limit) queue.push({ id: neighbor_id, depth: next_depth });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// D) Expansión vertical por ESCALERAS
|
||||
if(room_model.stairs){
|
||||
for(const sid of room_model.stairs){
|
||||
const stair_model = hollow_bond.map.stairs ? hollow_bond.map.stairs[sid] : null;
|
||||
if(!stair_model || !stair_model.rooms) continue;
|
||||
|
||||
for(const neighbor_id of stair_model.rooms){
|
||||
if(neighbor_id && neighbor_id !== id && room_depths[neighbor_id] === undefined){
|
||||
const next_depth = depth + 1;
|
||||
room_depths[neighbor_id] = next_depth;
|
||||
if(next_depth <= unload_limit) queue.push({ id: neighbor_id, depth: next_depth });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Instanciar salas dentro del rango de carga
|
||||
for(const rid in room_depths){
|
||||
const depth = room_depths[rid];
|
||||
if(depth <= load_limit){
|
||||
if(!self.objects.room[rid]){
|
||||
const model = hollow_bond.map.rooms ? hollow_bond.map.rooms[rid] : null;
|
||||
if(model){
|
||||
const room_obj = new RoomObjectGame(
|
||||
hollow_bond,
|
||||
model,
|
||||
self.geometries.room,
|
||||
self.materials.room
|
||||
);
|
||||
self.objects.room[rid] = room_obj;
|
||||
self.scene.add(room_obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Instanciar marcos de las salas cargadas
|
||||
for(const rid in self.objects.room){
|
||||
const r_obj = self.objects.room[rid];
|
||||
if(r_obj.model && r_obj.model.frames){
|
||||
for(const fid of r_obj.model.frames){
|
||||
if(!self.objects.frame[fid]){
|
||||
const frame_model = hollow_bond.map.frames ? hollow_bond.map.frames[fid] : null;
|
||||
if(frame_model){
|
||||
const frame_obj = new FrameObjectGame(
|
||||
hollow_bond,
|
||||
frame_model,
|
||||
self.geometries.frame,
|
||||
self.materials.frame
|
||||
);
|
||||
self.objects.frame[fid] = frame_obj;
|
||||
self.scene.add(frame_obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Instanciar huecos de las salas cargadas
|
||||
for(const rid in self.objects.room){
|
||||
const r_obj = self.objects.room[rid];
|
||||
if(r_obj.model && r_obj.model.holes){
|
||||
for(const hid of r_obj.model.holes){
|
||||
if(!self.objects.hole[hid]){
|
||||
const hole_model = hollow_bond.map.holes ? hollow_bond.map.holes[hid] : null;
|
||||
if(hole_model){
|
||||
const hole_obj = new HoleObjectGame(
|
||||
hollow_bond,
|
||||
hole_model,
|
||||
self.geometries.hole,
|
||||
self.materials.hole
|
||||
);
|
||||
self.objects.hole[hid] = hole_obj;
|
||||
self.scene.add(hole_obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Instanciar escaleras de los huecos / salas cargadas
|
||||
for(const rid in self.objects.room){
|
||||
const r_obj = self.objects.room[rid];
|
||||
|
||||
// Escaleras alojadas dentro de los huecos de la sala
|
||||
if(r_obj.model && r_obj.model.holes){
|
||||
for(const hid of r_obj.model.holes){
|
||||
const hole_model = hollow_bond.map.holes ? hollow_bond.map.holes[hid] : null;
|
||||
if(hole_model && hole_model.stairs){
|
||||
for(const stair_model of hole_model.stairs){
|
||||
if(!self.objects.stair[stair_model.id]){
|
||||
const stair_obj = new StairObjectGame(
|
||||
hollow_bond,
|
||||
stair_model,
|
||||
self.materials.stair,
|
||||
self.geometries.stair
|
||||
);
|
||||
self.objects.stair[stair_model.id] = stair_obj;
|
||||
self.scene.add(stair_obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Escaleras registradas directamente en la sala
|
||||
if(r_obj.model && r_obj.model.stairs){
|
||||
for(const sid of r_obj.model.stairs){
|
||||
if(!self.objects.stair[sid]){
|
||||
const stair_model = hollow_bond.map.stairs ? hollow_bond.map.stairs[sid] : null;
|
||||
if(stair_model){
|
||||
const stair_obj = new StairObjectGame(
|
||||
hollow_bond,
|
||||
stair_model,
|
||||
self.materials.stair,
|
||||
self.geometries.stair
|
||||
);
|
||||
self.objects.stair[sid] = stair_obj;
|
||||
self.scene.add(stair_obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Descargar salas fuera de rango
|
||||
for(const rid in self.objects.room){
|
||||
const depth = room_depths[rid];
|
||||
if(depth === undefined || depth > unload_limit){
|
||||
if(self.current_room && self.current_room.model.id === rid) continue;
|
||||
|
||||
const obj = self.objects.room[rid];
|
||||
self.scene.remove(obj.mesh);
|
||||
obj.destroy && obj.destroy();
|
||||
delete self.objects.room[rid];
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Descargar marcos huérfanos
|
||||
for(const fid in self.objects.frame){
|
||||
const f_obj = self.objects.frame[fid];
|
||||
let is_needed = false;
|
||||
if(f_obj.model && f_obj.model.rooms){
|
||||
for(const rid of f_obj.model.rooms){
|
||||
if(rid && self.objects.room[rid]){
|
||||
is_needed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!is_needed){
|
||||
self.scene.remove(f_obj.mesh);
|
||||
f_obj.destroy && f_obj.destroy();
|
||||
delete self.objects.frame[fid];
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Instanciar ascensores de los chunks / mapa cargados
|
||||
for(const rid in self.objects.room){
|
||||
const r_obj = self.objects.room[rid];
|
||||
if(r_obj.model && r_obj.model.elevators){
|
||||
for(const eid of r_obj.model.elevators){
|
||||
if(!self.objects.elevator[eid]){
|
||||
const el_model = hollow_bond.map.elevators ? hollow_bond.map.elevators[eid] : null;
|
||||
if(el_model){
|
||||
const el_obj = new ElevatorObjectGame(
|
||||
hollow_bond,
|
||||
el_model,
|
||||
self.materials.elevator,
|
||||
self.geometries.elevator
|
||||
);
|
||||
self.objects.elevator[eid] = el_obj;
|
||||
self.scene.add(el_obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Descargar huecos huérfanos
|
||||
for(const hid in self.objects.hole){
|
||||
const h_obj = self.objects.hole[hid];
|
||||
let is_needed = false;
|
||||
if(h_obj.model && h_obj.model.rooms){
|
||||
for(const rid of h_obj.model.rooms){
|
||||
if(rid && self.objects.room[rid]){
|
||||
is_needed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!is_needed){
|
||||
self.scene.remove(h_obj.mesh);
|
||||
h_obj.destroy && h_obj.destroy();
|
||||
delete self.objects.hole[hid];
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Descargar escaleras huérfanas
|
||||
for(const sid in self.objects.stair){
|
||||
const s_obj = self.objects.stair[sid];
|
||||
let is_needed = false;
|
||||
|
||||
if(s_obj.model){
|
||||
// Si su foso sigue cargado en escena
|
||||
if(s_obj.model.hole_id && self.objects.hole[s_obj.model.hole_id]){
|
||||
is_needed = true;
|
||||
} else if(s_obj.model.rooms){
|
||||
for(const rid of s_obj.model.rooms){
|
||||
if(rid && self.objects.room[rid]){
|
||||
is_needed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!is_needed){
|
||||
self.scene.remove(s_obj.mesh);
|
||||
s_obj.destroy && s_obj.destroy();
|
||||
delete self.objects.stair[sid];
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Descargar ascensores huérfanos
|
||||
for(const eid in self.objects.elevator){
|
||||
const el_obj = self.objects.elevator[eid];
|
||||
let is_needed = false;
|
||||
|
||||
for(const rid in self.objects.room){
|
||||
const r_model = self.objects.room[rid].model;
|
||||
if(r_model && r_model.elevators && r_model.elevators.includes(eid)){
|
||||
is_needed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!is_needed){
|
||||
self.scene.remove(el_obj.mesh);
|
||||
el_obj.destroy && el_obj.destroy();
|
||||
delete self.objects.elevator[eid];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback de integración con MapManager cuando se genera un Chunk.
|
||||
*/
|
||||
this.sync_chunk = chunk => {
|
||||
if(self.on && self.current_room){
|
||||
self.update_rooms_streaming();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback de integración con MapManager cuando se descarga un Chunk.
|
||||
*/
|
||||
this.remove_chunk = chunk => {
|
||||
if(self.on && self.current_room){
|
||||
self.update_rooms_streaming();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detiene el bucle y limpia la sesión.
|
||||
*/
|
||||
this.stop = () => {
|
||||
self.on = false;
|
||||
if(animation_frame_id){
|
||||
cancelAnimationFrame(animation_frame_id);
|
||||
}
|
||||
if(debug_hud){
|
||||
debug_hud.destroy();
|
||||
debug_hud = null;
|
||||
}
|
||||
self.reset();
|
||||
};
|
||||
|
||||
/**
|
||||
* Limpia todos los objetos 3D y buffers de GPU.
|
||||
*/
|
||||
this.reset = () => {
|
||||
|
||||
if(debug_hud){
|
||||
debug_hud.destroy();
|
||||
debug_hud = null;
|
||||
}
|
||||
|
||||
for(const key in self.objects){
|
||||
for(const id in self.objects[key]){
|
||||
self.scene && self.scene.remove(self.objects[key][id].mesh);
|
||||
self.objects[key][id].destroy && self.objects[key][id].destroy();
|
||||
}
|
||||
self.objects[key] = {};
|
||||
}
|
||||
|
||||
for(const key in self.geometries){
|
||||
if(self.geometries[key] && self.geometries[key].reset){
|
||||
self.geometries[key].reset();
|
||||
}
|
||||
}
|
||||
|
||||
for(const key in self.materials){
|
||||
if(self.materials[key] && self.materials[key].reset){
|
||||
self.materials[key].reset();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setup_scene = () => {
|
||||
self.scene = new THREE.Scene();
|
||||
self.scene.background = new THREE.Color(0x050505);
|
||||
|
||||
// Niebla volumétrica ligera acorde a los Backrooms
|
||||
self.scene.fog = new THREE.FogExp2(0x050505, 0.035);
|
||||
|
||||
self.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
|
||||
|
||||
self.renderer = new THREE.WebGLRenderer({
|
||||
antialias : hollow_bond.render_scale >= 100,
|
||||
powerPreference : "high-performance"
|
||||
});
|
||||
|
||||
self.renderer.domElement.className = "hb-canvas-dungeon";
|
||||
self.renderer.domElement.tabIndex = 1;
|
||||
hollow_bond.item_self.appendChild(self.renderer.domElement);
|
||||
|
||||
self.update_resolution();
|
||||
|
||||
const ambient = new THREE.AmbientLight(0xfff5e6, 0.45);
|
||||
self.scene.add(ambient);
|
||||
|
||||
// window.addEventListener("keydown", e => {
|
||||
// if(e.code === "KeyE" && self.camera){
|
||||
// for(const eid in self.objects.elevator){
|
||||
// if(self.objects.elevator[eid].interact(self.camera.position)){
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
// Interacción por click en el botón del ascensor
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const center_screen = new THREE.Vector2(0, 0); // Centro exacto del punto de mira
|
||||
|
||||
window.addEventListener("mousedown", e => {
|
||||
// Solo botón izquierdo del ratón (button === 0)
|
||||
if(e.button !== 0 || !self.camera || !self.on) return;
|
||||
|
||||
raycaster.setFromCamera(center_screen, self.camera);
|
||||
|
||||
for(const eid in self.objects.elevator){
|
||||
const el_obj = self.objects.elevator[eid];
|
||||
if(el_obj.try_click_button(raycaster)){
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("resize", self.update_resolution);
|
||||
};
|
||||
|
||||
this.update_resolution = () => {
|
||||
if(!self.renderer || !self.camera) return;
|
||||
const w = window.innerWidth,
|
||||
h = window.innerHeight,
|
||||
scale = Math.max(0.03125, hollow_bond.render_scale / 100);
|
||||
|
||||
self.camera.aspect = w / h;
|
||||
self.camera.updateProjectionMatrix();
|
||||
self.renderer.setSize(Math.floor(w * scale), Math.floor(h * scale), false);
|
||||
self.renderer.domElement.style.width = "100%";
|
||||
self.renderer.domElement.style.height = "100%";
|
||||
};
|
||||
|
||||
const render_loop = () => {
|
||||
if(!self.on) return;
|
||||
|
||||
animation_frame_id = requestAnimationFrame(render_loop);
|
||||
|
||||
const now = performance.now(),
|
||||
delta = Math.min((now - last_frame_time) / 1000, 0.1);
|
||||
|
||||
last_frame_time = now;
|
||||
|
||||
// 1. Detección y transición entre habitaciones
|
||||
check_room_transition();
|
||||
|
||||
// // 2. Físicas y movimiento del jugador
|
||||
// if(hollow_bond.controls && hollow_bond.controls.game){
|
||||
// hollow_bond.controls.game.update(self, delta);
|
||||
|
||||
// // Colisiones contra escaleras instanciadas
|
||||
// for(const sid in self.objects.stair){
|
||||
// self.objects.stair[sid].collider.resolve_player(
|
||||
// self.camera.position,
|
||||
// hollow_bond.controls.game,
|
||||
// 0.22
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// 2. Físicas y movimiento del jugador
|
||||
if(hollow_bond.controls && hollow_bond.controls.game){
|
||||
hollow_bond.controls.game.update(self, delta);
|
||||
|
||||
// Colisiones contra escaleras instanciadas
|
||||
for(const sid in self.objects.stair){
|
||||
self.objects.stair[sid].collider.resolve_player(
|
||||
self.camera.position,
|
||||
hollow_bond.controls.game,
|
||||
0.22
|
||||
);
|
||||
}
|
||||
|
||||
// Animación y colisiones de ascensores instanciados
|
||||
for(const eid in self.objects.elevator){
|
||||
const el_obj = self.objects.elevator[eid];
|
||||
el_obj.update(delta);
|
||||
el_obj.collider.resolve_player(
|
||||
self.camera.position,
|
||||
hollow_bond.controls.game,
|
||||
hollow_bond.controls.game.player_radius
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizamos la telemetría en cada frame
|
||||
debug_hud && debug_hud.tick();
|
||||
|
||||
if(hollow_bond.show_fps && hollow_bond.views && hollow_bond.views.fps){
|
||||
hollow_bond.views.fps.tick();
|
||||
}
|
||||
|
||||
self.renderer.render(self.scene, self.camera);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return DungeonManager;
|
||||
})();
|
||||
85
Public/ecma/Managers/HashManager.ecma.js
Normal file
85
Public/ecma/Managers/HashManager.ecma.js
Normal file
@ -0,0 +1,85 @@
|
||||
"use strict";
|
||||
|
||||
const HashManager = (function(){
|
||||
|
||||
const HashManager = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
let hash_prime_xf = hollow_bond.settings.get("hash_prime_xf", null, 0x1B873593),
|
||||
hash_prime_yf = hollow_bond.settings.get("hash_prime_yf", null, 0x85EBCA6B),
|
||||
hash_prime_zf = hollow_bond.settings.get("hash_prime_zf", null, 0xC2B2AE35),
|
||||
hash_prime_xt = hollow_bond.settings.get("hash_prime_xt", null, 0x3C6EF372),
|
||||
hash_prime_yt = hollow_bond.settings.get("hash_prime_yt", null, 0xBB67AE85),
|
||||
hash_prime_zt = hollow_bond.settings.get("hash_prime_zt", null, 0xEECE660D),
|
||||
hash_prime_channel = hollow_bond.settings.get("hash_prime_channel", null, 0x9E3779B9),
|
||||
hash_channel_chunk = hollow_bond.settings.get("hash_channel_chunk", null, 101),
|
||||
hash_channel_chunk_presence = hollow_bond.settings.get("hash_channel_chunk_presence", null, 105),
|
||||
hash_channel_seam_horizontal = hollow_bond.settings.get("hash_channel_seam_horizontal", null, 102),
|
||||
hash_channel_seam_vertical_x = hollow_bond.settings.get("hash_channel_seam_vertical_x", null, 103),
|
||||
hash_channel_seam_vertical_z = hollow_bond.settings.get("hash_channel_seam_vertical_z", null, 104),
|
||||
hash_channel_elevator = hollow_bond.settings.get("hash_channel_elevator", null, 201),
|
||||
hash_channel_hole = hollow_bond.settings.get("hash_channel_hole", null, 202),
|
||||
hash_channel_hole_internal = hollow_bond.settings.get("hash_channel_hole_internal", null, 203),
|
||||
hash_channel_room = hollow_bond.settings.get("hash_channel_room", null, 301),
|
||||
hash_channel_frame = hollow_bond.settings.get("hash_channel_frame", null, 401),
|
||||
hash_channel_stairs = hollow_bond.settings.get("hash_channel_stairs", null, 501),
|
||||
hash_channel_room_cut_direction = hollow_bond.settings.get("hash_channel_room_cut_direction", null, 601);
|
||||
|
||||
/**
|
||||
* Reduce una caja ortoédrica y un canal a un número pseudoaleatorio [0, 1) determinista.
|
||||
* @param {!number} xf
|
||||
* @param {!number} yf
|
||||
* @param {!number} zf
|
||||
* @param {!number} xt
|
||||
* @param {!number} yt
|
||||
* @param {!number} zt
|
||||
* @param {!number} channel
|
||||
* @param {number} [i = 0]
|
||||
* @returns {number}
|
||||
*/
|
||||
this.box = (xf, yf, zf, xt, yt, zt, channel, i = 0) => Common.random(hollow_bond.seed, (
|
||||
Math.imul(xf | 0, hash_prime_xf) ^
|
||||
Math.imul(yf | 0, hash_prime_yf) ^
|
||||
Math.imul(zf | 0, hash_prime_zf) ^
|
||||
Math.imul(xt | 0, hash_prime_xt) ^
|
||||
Math.imul(yt | 0, hash_prime_yt) ^
|
||||
Math.imul(zt | 0, hash_prime_zt) ^
|
||||
Math.imul(channel | 0, hash_prime_channel) ^
|
||||
(i | 0)
|
||||
) | 0);
|
||||
|
||||
// Métodos semánticos directos
|
||||
this.chunk = (xf, yf, zf, xt, yt, zt, i = 0) => self.box(xf, yf, zf, xt, yt, zt, hash_channel_chunk, i);
|
||||
|
||||
this.chunk_presence = (cx, cy, cz, i = 0) => self.box(cx, cy, cz, cx, cy, cz, hash_channel_chunk_presence, i);
|
||||
|
||||
this.seam_horizontal = (xf, y, zf, xt, zt, i = 0) => self.box(xf, y, zf, xt, y, zt, hash_channel_seam_horizontal, i);
|
||||
|
||||
this.seam_vertical_x = (x, yf, zf, yt, zt, i = 0) => self.box(x, yf, zf, x, yt, zt, hash_channel_seam_vertical_x, i);
|
||||
|
||||
this.seam_vertical_z = (xf, yf, z, xt, yt, i = 0) => self.box(xf, yf, z, xt, yt, z, hash_channel_seam_vertical_z, i);
|
||||
|
||||
this.elevator = (xf, yf, zf, xt, yt, zt, i = 0) => self.box(xf, yf, zf, xt, yt, zt, hash_channel_elevator, i);
|
||||
|
||||
this.seam_elevator = (xf, y, zf, xt, zt, i = 0) => self.box(xf, y, zf, xt, y, zt, hash_channel_elevator, i);
|
||||
|
||||
this.hole = (xf, yf, zf, xt, yt, zt, i = 0) => self.box(xf, yf, zf, xt, yt, zt, hash_channel_hole, i);
|
||||
|
||||
this.hole_internal = (xf, yf, zf, xt, yt, zt, i = 0) => self.box(xf, yf, zf, xt, yt, zt, hash_channel_hole_internal, i);
|
||||
|
||||
this.room = (xf, yf, zf, xt, yt, zt, i = 0) => self.box(xf, yf, zf, xt, yt, zt, hash_channel_room, i);
|
||||
|
||||
this.rooms = (a, b, c, d = 0, e = null, f = null, g = 0) => (
|
||||
e === null || f === null ? self.box(a, b, c, a, b, c, hash_channel_room, d) :
|
||||
self.box(a, b, c, d, e, f, hash_channel_room, g));
|
||||
|
||||
this.frame = (xf, yf, zf, xt, yt, zt, i = 0) => self.box(xf, yf, zf, xt, yt, zt, hash_channel_frame, i);
|
||||
|
||||
this.stairs = (xf, yf, zf, xt, yt, zt, i = 0) => self.box(xf, yf, zf, xt, yt, zt, hash_channel_stairs, i);
|
||||
|
||||
this.room_cut_axis = (py, a, b) => self.box(a[0] == "z" ? 71 : 70, a[1], py, b[0] == "z" ? 91 : 90, b[1], py, hash_channel_room_cut_direction, (1 << a[2]) | (1 << b[2]));
|
||||
|
||||
};
|
||||
|
||||
return HashManager;
|
||||
})();
|
||||
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(hollow_bond){
|
||||
|
||||
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(hollow_bond.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;
|
||||
})();
|
||||
484
Public/ecma/Managers/InspectorManager.ecma.js
Normal file
484
Public/ecma/Managers/InspectorManager.ecma.js
Normal file
@ -0,0 +1,484 @@
|
||||
"use strict";
|
||||
|
||||
const InspectorManager = (function(){
|
||||
|
||||
const InspectorManager = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
this.renderer = null;
|
||||
this.on = false;
|
||||
|
||||
// Configuración de visualización vertical
|
||||
this.current_floor = 0;
|
||||
this.vertical_layers_range = 2; // Cantidad de pisos arriba y abajo a mostrar
|
||||
this.floor_height = hollow_bond.room_height || 3;
|
||||
|
||||
this.geometries = {
|
||||
frame : new FrameGeometryDebug(hollow_bond),
|
||||
room : new RoomGeometryDebug(hollow_bond),
|
||||
hole : new HoleGeometryDebug(hollow_bond),
|
||||
stair : new StairGeometryDebug(hollow_bond),
|
||||
elevator : new ElevatorGeometryDebug(hollow_bond)
|
||||
};
|
||||
|
||||
this.materials = {
|
||||
frame : new FrameMaterialDebug(),
|
||||
room : new RoomMaterialDebug(),
|
||||
hole : new HoleMaterialDebug(),
|
||||
stair : new StairMaterialDebug(),
|
||||
elevator : new ElevatorMaterialDebug()
|
||||
};
|
||||
|
||||
// Diccionarios de objetos visuales en la escena
|
||||
this.objects = {
|
||||
frame : {},
|
||||
room : {},
|
||||
hole : {},
|
||||
stair : {},
|
||||
elevator : {}
|
||||
};
|
||||
|
||||
let floor_indicator_plane = null,
|
||||
hud_element = null;
|
||||
|
||||
let last_frame_time = performance.now();
|
||||
|
||||
/**
|
||||
* Inicializa la vista de inspección con vuelo libre.
|
||||
*/
|
||||
this.start = () => {
|
||||
self.on = true;
|
||||
setup_scene();
|
||||
setup_hud();
|
||||
hollow_bond.controls.debug.setup(self);
|
||||
|
||||
// Poblamos entidades que ya existan en el mapa
|
||||
self.synchronize_all();
|
||||
|
||||
// Activamos contador de FPS si está habilitado
|
||||
hollow_bond.views.fps.go();
|
||||
|
||||
render_loop();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sincroniza todas las entidades disponibles en el mapa global.
|
||||
*/
|
||||
this.synchronize_all = () => {
|
||||
synchronize_holes();
|
||||
synchronize_elevators();
|
||||
synchronize_stairs();
|
||||
synchronize_frames();
|
||||
synchronize_rooms();
|
||||
self.update_layer_visibility();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sincroniza de forma incremental un Chunk específico recién generado.
|
||||
* @param {ChunkModel} chunk
|
||||
*/
|
||||
this.sync_chunk = chunk => {
|
||||
if(!chunk) return;
|
||||
|
||||
// 1. Huecos
|
||||
if(chunk.holes){
|
||||
for(const hid in chunk.holes){
|
||||
if(!self.objects.hole[hid]){
|
||||
const model = chunk.holes[hid];
|
||||
const obj = new HoleObjectDebug(hollow_bond, model, self.geometries.hole, self.materials.hole);
|
||||
obj.model = model;
|
||||
self.objects.hole[hid] = obj;
|
||||
self.scene.add(obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Ascensores
|
||||
if(chunk.elevators){
|
||||
for(const eid in chunk.elevators){
|
||||
if(!self.objects.elevator[eid]){
|
||||
const model = chunk.elevators[eid];
|
||||
const obj = new ElevatorObjectDebug(hollow_bond, model, self.geometries.elevator, self.materials.elevator);
|
||||
obj.model = model;
|
||||
self.objects.elevator[eid] = obj;
|
||||
self.scene.add(obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Escaleras
|
||||
if(chunk.stairs){
|
||||
for(const sid in chunk.stairs){
|
||||
if(!self.objects.stair[sid]){
|
||||
const model = chunk.stairs[sid];
|
||||
const obj = new StairObjectDebug(hollow_bond, model);
|
||||
obj.model = model;
|
||||
self.objects.stair[sid] = obj;
|
||||
self.scene.add(obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Marcos (con visibilidad de capa inmediata)
|
||||
if(chunk.frames){
|
||||
for(const fid in chunk.frames){
|
||||
if(!self.objects.frame[fid]){
|
||||
const model = chunk.frames[fid];
|
||||
const obj = new FrameObjectDebug(hollow_bond, model, self.geometries.frame, self.materials.frame);
|
||||
obj.model = model;
|
||||
|
||||
const dist = Math.abs(model.y - self.current_floor);
|
||||
if(dist > self.vertical_layers_range){
|
||||
obj.mesh.visible = false;
|
||||
} else {
|
||||
obj.mesh.visible = true;
|
||||
obj.set_layer_distance && obj.set_layer_distance(dist);
|
||||
}
|
||||
|
||||
self.objects.frame[fid] = obj;
|
||||
self.scene.add(obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Habitaciones (con visibilidad de capa inmediata)
|
||||
if(chunk.rooms){
|
||||
for(const rid in chunk.rooms){
|
||||
if(!self.objects.room[rid]){
|
||||
const model = chunk.rooms[rid];
|
||||
const obj = new RoomObjectDebug(hollow_bond, model);
|
||||
obj.model = model;
|
||||
|
||||
const floor_index = Math.floor(model.y / self.floor_height);
|
||||
const dist = Math.abs(floor_index - self.current_floor);
|
||||
obj.mesh.visible = (dist <= self.vertical_layers_range);
|
||||
|
||||
self.objects.room[rid] = obj;
|
||||
self.scene.add(obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retira y destruye las mallas 3D asociadas a un Chunk descargado.
|
||||
* @param {ChunkModel} chunk
|
||||
*/
|
||||
this.remove_chunk = chunk => {
|
||||
if(!chunk) return;
|
||||
|
||||
const remove_group = (type, source) => {
|
||||
if(!source) return;
|
||||
for(const id in source){
|
||||
if(self.objects[type][id]){
|
||||
self.scene && self.scene.remove(self.objects[type][id].mesh);
|
||||
self.objects[type][id].destroy && self.objects[type][id].destroy();
|
||||
delete self.objects[type][id];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
remove_group("frame", chunk.frames);
|
||||
remove_group("room", chunk.rooms);
|
||||
remove_group("hole", chunk.holes);
|
||||
remove_group("elevator", chunk.elevators);
|
||||
remove_group("stair", chunk.stairs);
|
||||
};
|
||||
|
||||
/**
|
||||
* Limpia por completo la escena y libera memoria de GPU.
|
||||
*/
|
||||
this.reset = () => {
|
||||
|
||||
// 1. Limpieza de mallas en la escena
|
||||
for(const key in self.objects){
|
||||
for(const id in self.objects[key]){
|
||||
self.scene && self.scene.remove(self.objects[key][id].mesh);
|
||||
self.objects[key][id].destroy && self.objects[key][id].destroy();
|
||||
};
|
||||
self.objects[key] = {};
|
||||
};
|
||||
|
||||
// 2. Liberación de buffers geométricos
|
||||
for(const key in self.geometries)
|
||||
self.geometries[key].reset && self.geometries[key].reset();
|
||||
|
||||
// 3. Liberación de shaders y materiales
|
||||
for(const key in self.materials)
|
||||
self.materials[key].reset && self.materials[key].reset();
|
||||
|
||||
};
|
||||
|
||||
const setup_scene = () => {
|
||||
const chunk_height = hollow_bond.chunk_size * hollow_bond.room_height;
|
||||
|
||||
self.scene = new THREE.Scene();
|
||||
self.scene.background = new THREE.Color(0x0a0a0d);
|
||||
|
||||
self.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1500);
|
||||
|
||||
self.renderer = new THREE.WebGLRenderer({
|
||||
antialias : hollow_bond.render_scale >= 100,
|
||||
powerPreference : "high-performance"
|
||||
});
|
||||
|
||||
self.renderer.domElement.className = "hb-canvas-inspector";
|
||||
self.renderer.domElement.tabIndex = 1;
|
||||
hollow_bond.item_self.appendChild(self.renderer.domElement);
|
||||
|
||||
self.update_resolution();
|
||||
|
||||
// Iluminación
|
||||
self.scene.add(new THREE.AmbientLight(0xffffff, 0.85));
|
||||
const sun = new THREE.DirectionalLight(0xd6d0ba, 1.2);
|
||||
sun.position.set(25, 60, 25);
|
||||
self.scene.add(sun);
|
||||
|
||||
// Cajas guía de los Chunks iniciales
|
||||
[-1, 0, 1].forEach(cy => {
|
||||
const wire_box = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(hollow_bond.chunk_size, chunk_height, hollow_bond.chunk_size),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color : cy === 0 ? 0x3d4450 : 0x1f232b,
|
||||
wireframe : true,
|
||||
transparent : true,
|
||||
opacity : cy === 0 ? 0.35 : 0.15
|
||||
})
|
||||
);
|
||||
wire_box.position.set(
|
||||
hollow_bond.chunk_size / 2,
|
||||
(cy * chunk_height) + (chunk_height / 2),
|
||||
hollow_bond.chunk_size / 2
|
||||
);
|
||||
self.scene.add(wire_box);
|
||||
});
|
||||
|
||||
// Plano visual sutil que marca la cota del piso seleccionado
|
||||
floor_indicator_plane = new THREE.GridHelper(hollow_bond.chunk_size * 3, hollow_bond.chunk_size * 3, 0x00bcd4, 0x1e293b);
|
||||
floor_indicator_plane.position.set(hollow_bond.chunk_size / 2, 0, hollow_bond.chunk_size / 2);
|
||||
self.scene.add(floor_indicator_plane);
|
||||
|
||||
// Posición inicial de la cámara
|
||||
self.camera.position.set(hollow_bond.chunk_size / 2, 5, hollow_bond.chunk_size / 2 + 10);
|
||||
|
||||
window.addEventListener("resize", self.update_resolution);
|
||||
};
|
||||
|
||||
this.update_resolution = () => {
|
||||
if(!self.renderer || !self.camera) return;
|
||||
const w = window.innerWidth,
|
||||
h = window.innerHeight,
|
||||
scale = Math.max(0.03125, hollow_bond.render_scale / 100);
|
||||
|
||||
self.camera.aspect = w / h;
|
||||
self.camera.updateProjectionMatrix();
|
||||
self.renderer.setSize(Math.floor(w * scale), Math.floor(h * scale), false);
|
||||
self.renderer.domElement.style.width = "100%";
|
||||
self.renderer.domElement.style.height = "100%";
|
||||
};
|
||||
|
||||
/**
|
||||
* Ajusta la cota activa y recalcula las opacidades verticales.
|
||||
*/
|
||||
this.set_floor = floor_y => {
|
||||
self.current_floor = floor_y;
|
||||
|
||||
if(floor_indicator_plane){
|
||||
floor_indicator_plane.position.y = self.current_floor * self.floor_height;
|
||||
}
|
||||
|
||||
self.update_layer_visibility();
|
||||
self.update_hud();
|
||||
};
|
||||
|
||||
/**
|
||||
* Aplica la visibilidad y opacidad degradada a marcos y habitaciones.
|
||||
*/
|
||||
this.update_layer_visibility = () => {
|
||||
const range = self.vertical_layers_range;
|
||||
|
||||
// 1. Filtrado y gradiente de marcos
|
||||
for(const id in self.objects.frame){
|
||||
const frame_obj = self.objects.frame[id],
|
||||
dist = Math.abs(frame_obj.model.y - self.current_floor);
|
||||
|
||||
if(dist > range){
|
||||
frame_obj.mesh.visible = false;
|
||||
} else {
|
||||
frame_obj.mesh.visible = true;
|
||||
frame_obj.set_layer_distance && frame_obj.set_layer_distance(dist);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Filtrado de habitaciones
|
||||
for(const id in self.objects.room){
|
||||
const room_obj = self.objects.room[id],
|
||||
floor_index = Math.floor(room_obj.model.y / self.floor_height),
|
||||
dist = Math.abs(floor_index - self.current_floor);
|
||||
|
||||
room_obj.mesh.visible = (dist <= range);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Sincronizadores Globales ---
|
||||
|
||||
const synchronize_frames = () => {
|
||||
if(!hollow_bond.map || !hollow_bond.map.frames) return;
|
||||
|
||||
for(const frame_id in hollow_bond.map.frames){
|
||||
if(!self.objects.frame[frame_id]){
|
||||
const frame_model = hollow_bond.map.frames[frame_id];
|
||||
const frame_obj = new FrameObjectDebug(
|
||||
hollow_bond,
|
||||
frame_model,
|
||||
self.geometries.frame,
|
||||
self.materials.frame
|
||||
);
|
||||
frame_obj.model = frame_model;
|
||||
|
||||
const dist = Math.abs(frame_model.y - self.current_floor);
|
||||
if(dist > self.vertical_layers_range){
|
||||
frame_obj.mesh.visible = false;
|
||||
} else {
|
||||
frame_obj.mesh.visible = true;
|
||||
frame_obj.set_layer_distance && frame_obj.set_layer_distance(dist);
|
||||
}
|
||||
|
||||
self.objects.frame[frame_id] = frame_obj;
|
||||
self.scene.add(frame_obj.mesh);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const synchronize_rooms = () => {
|
||||
if(!hollow_bond.map || !hollow_bond.map.rooms) return;
|
||||
|
||||
for(const room_id in hollow_bond.map.rooms){
|
||||
if(!self.objects.room[room_id]){
|
||||
const room_model = hollow_bond.map.rooms[room_id];
|
||||
const room_obj = new RoomObjectDebug(hollow_bond, room_model);
|
||||
room_obj.model = room_model;
|
||||
|
||||
const floor_index = Math.floor(room_model.y / self.floor_height),
|
||||
dist = Math.abs(floor_index - self.current_floor);
|
||||
room_obj.mesh.visible = (dist <= self.vertical_layers_range);
|
||||
|
||||
self.objects.room[room_id] = room_obj;
|
||||
self.scene.add(room_obj.mesh);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const synchronize_holes = () => {
|
||||
if(!hollow_bond.map || !hollow_bond.map.chunks) return;
|
||||
|
||||
for(const cid in hollow_bond.map.chunks){
|
||||
const chunk = hollow_bond.map.chunks[cid];
|
||||
if(!chunk || !chunk.holes) continue;
|
||||
|
||||
for(const hid in chunk.holes){
|
||||
if(!self.objects.hole[hid]){
|
||||
const hole_model = chunk.holes[hid];
|
||||
const hole_obj = new HoleObjectDebug(
|
||||
hollow_bond,
|
||||
hole_model,
|
||||
self.geometries.hole,
|
||||
self.materials.hole
|
||||
);
|
||||
hole_obj.model = hole_model;
|
||||
|
||||
self.objects.hole[hid] = hole_obj;
|
||||
self.scene.add(hole_obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const synchronize_elevators = () => {
|
||||
if(!hollow_bond.map || !hollow_bond.map.chunks) return;
|
||||
|
||||
for(const cid in hollow_bond.map.chunks){
|
||||
const chunk = hollow_bond.map.chunks[cid];
|
||||
if(!chunk || !chunk.elevators) continue;
|
||||
|
||||
for(const eid in chunk.elevators){
|
||||
if(!self.objects.elevator[eid]){
|
||||
const elevator_model = chunk.elevators[eid];
|
||||
const elevator_obj = new ElevatorObjectDebug(
|
||||
hollow_bond,
|
||||
elevator_model,
|
||||
self.geometries.elevator,
|
||||
self.materials.elevator
|
||||
);
|
||||
elevator_obj.model = elevator_model;
|
||||
|
||||
self.objects.elevator[eid] = elevator_obj;
|
||||
self.scene.add(elevator_obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const synchronize_stairs = () => {
|
||||
if(!hollow_bond.map || !hollow_bond.map.chunks) return;
|
||||
|
||||
for(const cid in hollow_bond.map.chunks){
|
||||
const chunk = hollow_bond.map.chunks[cid];
|
||||
if(!chunk || !chunk.stairs) continue;
|
||||
|
||||
for(const sid in chunk.stairs){
|
||||
if(!self.objects.stair[sid]){
|
||||
const stair_model = chunk.stairs[sid];
|
||||
const stair_obj = new StairObjectDebug(hollow_bond, stair_model);
|
||||
stair_obj.model = stair_model;
|
||||
|
||||
self.objects.stair[sid] = stair_obj;
|
||||
self.scene.add(stair_obj.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setup_hud = () => {
|
||||
hud_element = hollow_bond.html(hollow_bond.item_self, ["div", {"class" : "inspector-hud"}, [
|
||||
["div", {"class" : "hud-title"}, "MODO INSPECCIÓN TÉCNICA"],
|
||||
["div", {"class" : "hud-row"}, [
|
||||
["span", {}, "Piso enfocado (Y): "],
|
||||
["b", {"class" : "hud-floor-label"}, String(self.current_floor)]
|
||||
]],
|
||||
["div", {"class" : "hud-row"}, [
|
||||
["span", {}, "Capas adyacentes: ±"],
|
||||
["b", {"class" : "hud-range-label"}, String(self.vertical_layers_range)]
|
||||
]],
|
||||
["div", {"class" : "hud-controls-hint"}, "[Q/E] Subir/Bajar piso | [0..4] Rango de capas | [WASD+Ratón] Volar"]
|
||||
]])[0];
|
||||
};
|
||||
|
||||
this.update_hud = () => {
|
||||
if(!hud_element) return;
|
||||
hud_element.querySelector(".hud-floor-label").textContent = String(self.current_floor);
|
||||
hud_element.querySelector(".hud-range-label").textContent = String(self.vertical_layers_range);
|
||||
};
|
||||
|
||||
const render_loop = () => {
|
||||
requestAnimationFrame(render_loop);
|
||||
|
||||
const now = performance.now(),
|
||||
delta = Math.min((now - last_frame_time) / 1000, 0.1);
|
||||
|
||||
last_frame_time = now;
|
||||
|
||||
hollow_bond.controls.debug.update_movement(self, delta);
|
||||
|
||||
hollow_bond.show_fps && hollow_bond.views.fps && hollow_bond.views.fps.tick();
|
||||
|
||||
self.renderer.render(self.scene, self.camera);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return InspectorManager;
|
||||
})();
|
||||
285
Public/ecma/Managers/MapManager.ecma.js
Normal file
285
Public/ecma/Managers/MapManager.ecma.js
Normal file
@ -0,0 +1,285 @@
|
||||
"use strict";
|
||||
|
||||
const MapManager = (function(){
|
||||
|
||||
const MapManager = function(hollow_bond){
|
||||
|
||||
const self = this,
|
||||
frontier_selected_keys = new Set();
|
||||
let maximum_chunks = 0,
|
||||
maximum_complete_layer = 0,
|
||||
frontier_layer = 0;
|
||||
|
||||
this.current_chunk = {x : 0, y : 0, z : 0};
|
||||
|
||||
// Colección de Chunks activos indexados por "x_y_z"
|
||||
this.chunks = {};
|
||||
this.frames = {};
|
||||
this.rooms = {};
|
||||
this.holes = {};
|
||||
this.stairs = {};
|
||||
this.elevators = {};
|
||||
|
||||
const get_chunk_key = (x, y, z) => `${x}_${y}_${z}`;
|
||||
|
||||
this.get_chunk = (x, y, z) => self.chunks[get_chunk_key(x, y, z)] || null;
|
||||
|
||||
this.clear = () => {
|
||||
self.chunks = {};
|
||||
self.frames = {};
|
||||
self.rooms = {};
|
||||
self.holes = {};
|
||||
self.stairs = {};
|
||||
self.elevators = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Destruye y libera un Chunk de la memoria activa.
|
||||
*/
|
||||
const unload_chunk = (x, y, z) => {
|
||||
|
||||
const key = get_chunk_key(x, y, z),
|
||||
visualizer = (
|
||||
hollow_bond.inspector.on ? hollow_bond.inspector :
|
||||
hollow_bond.dungeon.on ? hollow_bond.dungeon :
|
||||
null);
|
||||
|
||||
if(!self.chunks[key])
|
||||
return;
|
||||
|
||||
// Avisamos a DungeonManager si está activo para que elimine los meshes 3D
|
||||
visualizer && visualizer.remove_chunk(self.chunks[key]);
|
||||
|
||||
// Limpiamos referencias de frames y entidades de ese Chunk
|
||||
for(const id in self.chunks[key].frames) delete self.frames[id];
|
||||
for(const id in self.chunks[key].rooms) delete self.rooms[id];
|
||||
for(const id in self.chunks[key].holes) delete self.holes[id];
|
||||
for(const id in self.chunks[key].stairs) delete self.stairs[id];
|
||||
for(const id in self.chunks[key].elevators) delete self.elevators[id];
|
||||
|
||||
delete self.chunks[key];
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Actualiza el centro del streaming.
|
||||
* Carga los nuevos vecinos que toquen y descarga los que queden lejos.
|
||||
*/
|
||||
this.update_active_center = (cx, cy, cz, callback = null) => {
|
||||
|
||||
const newly_created = [],
|
||||
pending_targets = [
|
||||
[0, 0, 0], // Centro actual
|
||||
[-1, 0, 0], // Oeste
|
||||
[1, 0, 0], // Este
|
||||
[0, -1, 0], // Abajo
|
||||
[0, 1, 0], // Arriba
|
||||
[0, 0, -1], // Norte
|
||||
[0, 0, 1] // Sur
|
||||
].map(([ox, oy, oz]) => [cx + ox, cy + oy, cz + oz]).filter(([tx, ty, tz]) => (
|
||||
!self.chunks[get_chunk_key(tx, ty, tz)] && chunk_exists(tx, ty, tz)
|
||||
)),
|
||||
process_uploads = () => {
|
||||
for(const key in self.chunks)
|
||||
Math.max(Math.abs(self.chunks[key].x - cx), Math.abs(self.chunks[key].y - cy), Math.abs(self.chunks[key].z - cz)) > 2 &&
|
||||
unload_chunk(self.chunks[key].x, self.chunks[key].y, self.chunks[key].z);
|
||||
};
|
||||
|
||||
self.current_chunk.x = cx;
|
||||
self.current_chunk.y = cy;
|
||||
self.current_chunk.z = cz;
|
||||
|
||||
if(callback){
|
||||
|
||||
let i = 0;
|
||||
const process_next = () => {
|
||||
if(i < pending_targets.length)
|
||||
generate_chunk(...pending_targets[i ++], chunk => {
|
||||
chunk && newly_created.push(chunk);
|
||||
process_next();
|
||||
});
|
||||
else{
|
||||
process_uploads();
|
||||
callback(newly_created);
|
||||
};
|
||||
};
|
||||
|
||||
process_next();
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
for(const [tx, ty, tz] of pending_targets){
|
||||
|
||||
const chunk = generate_chunk(tx, ty, tz);
|
||||
|
||||
chunk && newly_created.push(chunk);
|
||||
|
||||
};
|
||||
|
||||
process_uploads();
|
||||
|
||||
return newly_created;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calcula el límite de capas completas y la selección exacta de la capa frontera.
|
||||
*/
|
||||
const setup_budget_layers = () => {
|
||||
|
||||
frontier_selected_keys.clear();
|
||||
|
||||
if(maximum_chunks <= 0)
|
||||
return;
|
||||
|
||||
// 1. Caso base: 1 Chunk exacto (solo el origen)
|
||||
if(maximum_chunks === 1){
|
||||
maximum_complete_layer = 0;
|
||||
frontier_layer = 0;
|
||||
return;
|
||||
};
|
||||
|
||||
// Buscamos si caben cubos completos mayores: (2*L + 1)^3
|
||||
let layer = 0, leftover;
|
||||
|
||||
// Capa 0 está completa (el Chunk 0, 0, 0)
|
||||
maximum_complete_layer = 0;
|
||||
frontier_layer = 1;
|
||||
|
||||
// for(; Math.pow(2 * (layer + 1) + 1, 3) <= maximum_chunks; layer ++);
|
||||
while(Math.pow(2 * (layer + 1) + 1, 3) <= maximum_chunks){
|
||||
layer++;
|
||||
}
|
||||
|
||||
maximum_complete_layer = layer; // Capa 100% llena
|
||||
frontier_layer = layer + 1; // Capa frontera
|
||||
|
||||
leftover = maximum_chunks - Math.pow(2 * maximum_complete_layer + 1, 3);
|
||||
|
||||
if(leftover <= 0)
|
||||
return;
|
||||
|
||||
// 2. Recolectamos todas las coordenadas de la cáscara cúbica frontier_layer
|
||||
const candidates = [];
|
||||
|
||||
for(let cx = -frontier_layer; cx <= frontier_layer; cx ++)
|
||||
for(let cy = -frontier_layer; cy <= frontier_layer; cy ++)
|
||||
for(let cz = -frontier_layer; cz <= frontier_layer; cz ++)
|
||||
Math.max(Math.abs(cx), Math.abs(cy), Math.abs(cz)) === frontier_layer && candidates.push({
|
||||
key : `${cx}_${cy}_${cz}`,
|
||||
manhattan : Math.abs(cx) + Math.abs(cy) + Math.abs(cz),
|
||||
score : hollow_bond.hash.chunk_presence(cx, cy, cz)
|
||||
});
|
||||
|
||||
// 3. Ordenamos: primero los que tienen contacto de cara (menor distancia Manhattan),
|
||||
// y dentro de ellos por su score determinista del hash.
|
||||
candidates.sort((a, b) => (
|
||||
a.manhattan !== b.manhattan ? a.manhattan - b.manhattan :
|
||||
a.score - b.score));
|
||||
|
||||
// 4. Seleccionamos exactamente 'leftover'
|
||||
for(let i = 0, count = Math.min(leftover, candidates.length); i < count; i ++)
|
||||
frontier_selected_keys.add(candidates[i].key);
|
||||
|
||||
};
|
||||
|
||||
const chunk_exists = (cx, cy, cz) => {
|
||||
// Modo Infinito
|
||||
if(maximum_chunks <= 0)
|
||||
return true;
|
||||
|
||||
const layer = Math.max(Math.abs(cx), Math.abs(cy), Math.abs(cz));
|
||||
|
||||
return (
|
||||
layer <= maximum_complete_layer ? true :
|
||||
layer > frontier_layer ? false :
|
||||
frontier_selected_keys.has(`${cx}_${cy}_${cz}`));
|
||||
};
|
||||
|
||||
const generate_chunk = (x, y, z, callback = null) => {
|
||||
|
||||
// Si el Chunk no existe en el presupuesto, no se crea
|
||||
if(!chunk_exists(x, y, z)){
|
||||
callback && callback();
|
||||
return null;
|
||||
};
|
||||
|
||||
const key = get_chunk_key(x, y, z);
|
||||
|
||||
if(self.chunks[key]){
|
||||
callback && callback();
|
||||
return self.chunks[key];
|
||||
};
|
||||
|
||||
const visualizer = (
|
||||
hollow_bond.inspector.on ? hollow_bond.inspector :
|
||||
hollow_bond.dungeon.on ? hollow_bond.dungeon :
|
||||
null),
|
||||
on_chunk_ready = () => {
|
||||
|
||||
Object.assign(self.frames, self.chunks[key].frames);
|
||||
Object.assign(self.rooms, self.chunks[key].rooms);
|
||||
Object.assign(self.holes, self.chunks[key].holes);
|
||||
Object.assign(self.stairs, self.chunks[key].stairs);
|
||||
Object.assign(self.elevators, self.chunks[key].elevators);
|
||||
|
||||
visualizer && visualizer.sync_chunk(self.chunks[key]);
|
||||
|
||||
callback && callback();
|
||||
|
||||
};
|
||||
|
||||
self.chunks[key] = new ChunkModel(hollow_bond, {
|
||||
x : x,
|
||||
y : y,
|
||||
z : z,
|
||||
neighbors : {
|
||||
west : chunk_exists(x - 1, y, z),
|
||||
east : chunk_exists(x + 1, y, z),
|
||||
down : chunk_exists(x, y - 1, z),
|
||||
up : chunk_exists(x, y + 1, z),
|
||||
north : chunk_exists(x, y, z - 1),
|
||||
south : chunk_exists(x, y, z + 1)
|
||||
}
|
||||
}, callback ? on_chunk_ready : null);
|
||||
|
||||
callback || on_chunk_ready();
|
||||
|
||||
return self.chunks[key];
|
||||
};
|
||||
|
||||
this.generate = (seed = null, new_maximum_chunks = 0, callback = null) => {
|
||||
self.clear();
|
||||
|
||||
if(seed !== null)
|
||||
hollow_bond.set_seed(seed);
|
||||
maximum_chunks = new_maximum_chunks;
|
||||
|
||||
setup_budget_layers();
|
||||
|
||||
if(callback)
|
||||
return new Promise(resolve => {
|
||||
self.update_active_center(0, 0, 0, () => {
|
||||
console.log(self.chunks);
|
||||
callback(self.chunks);
|
||||
resolve(self.chunks);
|
||||
});
|
||||
});
|
||||
|
||||
self.update_active_center(0, 0, 0);
|
||||
|
||||
console.log(self.chunks);
|
||||
|
||||
return self.chunks;
|
||||
};
|
||||
|
||||
this.generate_asynchronous = (seed = null, new_maximum_chunks = 0, callback = null) => self.generate(seed, new_maximum_chunks, callback || (() => {}));
|
||||
|
||||
const constructor = () => {};
|
||||
|
||||
constructor();
|
||||
|
||||
};
|
||||
|
||||
return MapManager;
|
||||
})();
|
||||
25
Public/ecma/Managers/ModelsManager.ecma.js
Normal file
25
Public/ecma/Managers/ModelsManager.ecma.js
Normal file
@ -0,0 +1,25 @@
|
||||
"use strict";
|
||||
|
||||
const ModelsManager = (function(){
|
||||
|
||||
const ModelsManager = function(hollow_bond){
|
||||
|
||||
const self = this,
|
||||
models = {};
|
||||
|
||||
const constructor = () => {};
|
||||
|
||||
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 ModelsManager;
|
||||
})();
|
||||
27
Public/ecma/Managers/SettingsManager.ecma.js
Normal file
27
Public/ecma/Managers/SettingsManager.ecma.js
Normal file
@ -0,0 +1,27 @@
|
||||
"use strict";
|
||||
|
||||
const SettingsManager = (function(){
|
||||
|
||||
const SettingsManager = function(hollow_bond){
|
||||
|
||||
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;
|
||||
})();
|
||||
54
Public/ecma/Managers/ThemesManager.ecma.js
Normal file
54
Public/ecma/Managers/ThemesManager.ecma.js
Normal file
@ -0,0 +1,54 @@
|
||||
"use strict";
|
||||
|
||||
const ThemeManager = (function(){
|
||||
|
||||
const ThemeManager = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
const themes = {};
|
||||
|
||||
// Tema activo por defecto
|
||||
this.current_theme = "level_0";
|
||||
|
||||
const constructor = () => {
|
||||
|
||||
// --- Registro del Tema Base: Level 0 (Backrooms Clásico) ---
|
||||
self.register(Level0Theme.key, Level0Theme);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Registra un nuevo tema gráfico.
|
||||
* @param {string} key - Identificador único ("level_0", "poolrooms", etc.)
|
||||
* @param {Object} definition - Colección de shaders por superficie
|
||||
*/
|
||||
this.register = (key, definition) => {
|
||||
themes[key] = definition;
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtiene la definición completa de un tema o el activo por defecto.
|
||||
* @param {string} [key=null]
|
||||
* @returns {Object}
|
||||
*/
|
||||
this.get = (key = null) => {
|
||||
const target_key = key || self.current_theme;
|
||||
return themes[target_key] || themes[self.current_theme] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cambia el tema global activo.
|
||||
* @param {string} key
|
||||
*/
|
||||
this.set_theme = key => {
|
||||
if(themes[key]){
|
||||
self.current_theme = key;
|
||||
}
|
||||
};
|
||||
|
||||
constructor();
|
||||
|
||||
};
|
||||
|
||||
return ThemeManager;
|
||||
})();
|
||||
116
Public/ecma/Materials/Debug/ElevatorMaterialDebug.ecma.js
Normal file
116
Public/ecma/Materials/Debug/ElevatorMaterialDebug.ecma.js
Normal file
@ -0,0 +1,116 @@
|
||||
"use strict";
|
||||
|
||||
const ElevatorMaterialDebug = (function(){
|
||||
|
||||
const ElevatorMaterialDebug = function(){
|
||||
|
||||
// Paleta semántica para distinguir columnas de ascensores
|
||||
const palette = [
|
||||
0xffeb3b, // Amarillo
|
||||
0x00e676, // Verde lima
|
||||
0x00e5ff, // Cian
|
||||
0xff4081, // Rosa/Magenta
|
||||
0xff9100, // Naranja
|
||||
0x7c4dff, // Violeta
|
||||
0x1de9b6 // Turquesa
|
||||
];
|
||||
|
||||
const color_cache = {},
|
||||
label_cache = {};
|
||||
let edge_mat = null;
|
||||
|
||||
/**
|
||||
* Devuelve un material coloreado determinista según el identificador de la columna/grupo.
|
||||
*/
|
||||
this.get_cabin_material = group_id => {
|
||||
const id_str = String(group_id || 0);
|
||||
|
||||
if(!color_cache[id_str]){
|
||||
// Hash determinista del ID para seleccionar color fijo de la paleta
|
||||
let hash = 0;
|
||||
for(let i = 0; i < id_str.length; i++){
|
||||
hash = (hash << 5) - hash + id_str.charCodeAt(i) | 0;
|
||||
}
|
||||
const color = palette[Math.abs(hash) % palette.length];
|
||||
|
||||
color_cache[id_str] = new THREE.MeshBasicMaterial({
|
||||
color : color,
|
||||
transparent : true,
|
||||
opacity : 0.65,
|
||||
wireframe : false
|
||||
});
|
||||
}
|
||||
|
||||
return color_cache[id_str];
|
||||
};
|
||||
|
||||
/**
|
||||
* Genera o recupera un material con el número de piso impreso en canvas para la puerta.
|
||||
*/
|
||||
this.get_door_label_material = floor_y => {
|
||||
const key = String(floor_y);
|
||||
|
||||
if(!label_cache[key]){
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 128;
|
||||
canvas.height = 128;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Fondo oscuro translúcido con marco
|
||||
ctx.fillStyle = "rgba(10, 12, 16, 0.9)";
|
||||
ctx.fillRect(0, 0, 128, 128);
|
||||
|
||||
ctx.strokeStyle = "#ffffff";
|
||||
ctx.lineWidth = 6;
|
||||
ctx.strokeRect(6, 6, 116, 116);
|
||||
|
||||
// Número de piso
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.font = "bold 52px monospace";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(String(floor_y), 64, 56);
|
||||
|
||||
// Pequeño indicador o icono de ascensor/puerta
|
||||
ctx.fillStyle = "#00bcd4";
|
||||
ctx.font = "bold 18px monospace";
|
||||
ctx.fillText("DOOR", 64, 98);
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
label_cache[key] = new THREE.MeshBasicMaterial({
|
||||
map : texture,
|
||||
transparent : true,
|
||||
opacity : 0.95
|
||||
});
|
||||
}
|
||||
|
||||
return label_cache[key];
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const key in color_cache){
|
||||
if(color_cache[key]){
|
||||
color_cache[key].dispose();
|
||||
delete color_cache[key];
|
||||
}
|
||||
}
|
||||
for(const key in label_cache){
|
||||
if(label_cache[key]){
|
||||
if(label_cache[key].map) label_cache[key].map.dispose();
|
||||
label_cache[key].dispose();
|
||||
delete label_cache[key];
|
||||
}
|
||||
}
|
||||
if(edge_mat){ edge_mat.dispose(); edge_mat = null; }
|
||||
};
|
||||
|
||||
this.get_edges = () => edge_mat || (edge_mat = new THREE.LineBasicMaterial({
|
||||
color : 0xffffff,
|
||||
transparent : true,
|
||||
opacity : 0.8
|
||||
}));
|
||||
|
||||
};
|
||||
|
||||
return ElevatorMaterialDebug;
|
||||
})();
|
||||
71
Public/ecma/Materials/Debug/FrameMaterialDebug.ecma.js
Normal file
71
Public/ecma/Materials/Debug/FrameMaterialDebug.ecma.js
Normal file
@ -0,0 +1,71 @@
|
||||
"use strict";
|
||||
|
||||
const FrameMaterialDebug = (function(){
|
||||
|
||||
const FrameMaterialDebug = function(){
|
||||
|
||||
const face_cache = {},
|
||||
edge_cache = {};
|
||||
|
||||
/**
|
||||
* Retorna el material de caras sólidas para la distancia al piso actual.
|
||||
* @param {number} dist - Distancia en pisos |model.y - current_floor|
|
||||
*/
|
||||
this.get_face_material = dist => {
|
||||
const key = String(dist);
|
||||
|
||||
if(!face_cache[key]){
|
||||
// Piso activo: 0.55 de opacidad; pisos lejanos: decae progresivamente
|
||||
const opacity = dist === 0 ? 0.55 : Math.max(0.08, 0.35 / (dist * 1.5));
|
||||
|
||||
face_cache[key] = new THREE.MeshBasicMaterial({
|
||||
color : dist === 0 ? 0xf1f5f9 : 0x94a3b8, // Blanco roto en foco, gris azulado fuera
|
||||
transparent : true,
|
||||
opacity : opacity,
|
||||
depthWrite : false
|
||||
});
|
||||
}
|
||||
|
||||
return face_cache[key];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retorna el material de aristas para la distancia al piso actual.
|
||||
* @param {number} dist - Distancia en pisos |model.y - current_floor|
|
||||
*/
|
||||
this.get_edge_material = dist => {
|
||||
const key = String(dist);
|
||||
|
||||
if(!edge_cache[key]){
|
||||
const opacity = dist === 0 ? 1.0 : Math.max(0.15, 0.6 / (dist * 1.4));
|
||||
|
||||
edge_cache[key] = new THREE.LineBasicMaterial({
|
||||
color : dist === 0 ? 0xffffff : 0xcbd5e1, // Blanco brillante en foco
|
||||
transparent : true,
|
||||
opacity : opacity,
|
||||
depthWrite : false
|
||||
});
|
||||
}
|
||||
|
||||
return edge_cache[key];
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const key in face_cache){
|
||||
if(face_cache[key]){
|
||||
face_cache[key].dispose();
|
||||
delete face_cache[key];
|
||||
}
|
||||
}
|
||||
for(const key in edge_cache){
|
||||
if(edge_cache[key]){
|
||||
edge_cache[key].dispose();
|
||||
delete edge_cache[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return FrameMaterialDebug;
|
||||
})();
|
||||
39
Public/ecma/Materials/Debug/HoleMaterialDebug.ecma.js
Normal file
39
Public/ecma/Materials/Debug/HoleMaterialDebug.ecma.js
Normal file
@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
|
||||
const HoleMaterialDebug = (function(){
|
||||
|
||||
const HoleMaterialDebug = function(){
|
||||
|
||||
let face_mat = null,
|
||||
edge_mat = null;
|
||||
|
||||
this.get = () => face_mat || (face_mat = new THREE.MeshBasicMaterial({
|
||||
color : 0x00bcd4,
|
||||
transparent : true,
|
||||
opacity : 0.25,
|
||||
wireframe : false,
|
||||
depthWrite : false
|
||||
}));
|
||||
|
||||
this.get_edges = () => edge_mat || (edge_mat = new THREE.LineBasicMaterial({
|
||||
color : 0x80deea, // Cian claro brillante
|
||||
transparent : true,
|
||||
opacity : 0.75,
|
||||
depthWrite : false
|
||||
}));
|
||||
|
||||
this.reset = () => {
|
||||
if(face_mat){
|
||||
face_mat.dispose();
|
||||
face_mat = null;
|
||||
}
|
||||
if(edge_mat){
|
||||
edge_mat.dispose();
|
||||
edge_mat = null;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return HoleMaterialDebug;
|
||||
})();
|
||||
76
Public/ecma/Materials/Debug/RoomMaterialDebug.ecma.js
Normal file
76
Public/ecma/Materials/Debug/RoomMaterialDebug.ecma.js
Normal file
@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
|
||||
const RoomMaterialDebug = (function(){
|
||||
|
||||
const RoomMaterialDebug = function(hollow_bond){
|
||||
|
||||
const face_cache = {},
|
||||
edge_cache = {};
|
||||
|
||||
/**
|
||||
* Material de las caras translúcidas de la habitación según la distancia al piso enfocado.
|
||||
* @param {number} [dist=0] - Distancia en pisos |floor - current_floor|
|
||||
* @returns {THREE.MeshBasicMaterial}
|
||||
*/
|
||||
this.get_face_material = (dist = 0) => {
|
||||
const key = String(dist);
|
||||
|
||||
if(!face_cache[key]){
|
||||
// Piso activo: opacidad moderada; pisos lejanos: decaimiento progresivo
|
||||
const opacity = dist === 0 ? 0.35 : Math.max(0.06, 0.22 / (dist * 1.5));
|
||||
|
||||
face_cache[key] = new THREE.MeshBasicMaterial({
|
||||
color : 0x9333ea, // Púrpura característico de habitaciones
|
||||
transparent : true,
|
||||
opacity : opacity,
|
||||
depthWrite : false
|
||||
});
|
||||
}
|
||||
|
||||
return face_cache[key];
|
||||
};
|
||||
|
||||
/**
|
||||
* Material para perfilar las aristas ortogonales de la habitación.
|
||||
* @param {number} [dist=0]
|
||||
* @returns {THREE.LineBasicMaterial}
|
||||
*/
|
||||
this.get_edge_material = (dist = 0) => {
|
||||
const key = String(dist);
|
||||
|
||||
if(!edge_cache[key]){
|
||||
const opacity = dist === 0 ? 0.85 : Math.max(0.12, 0.5 / (dist * 1.4));
|
||||
|
||||
edge_cache[key] = new THREE.LineBasicMaterial({
|
||||
color : 0xd8b4fe, // Lavanda brillante para aristas
|
||||
transparent : true,
|
||||
opacity : opacity,
|
||||
depthWrite : false
|
||||
});
|
||||
}
|
||||
|
||||
return edge_cache[key];
|
||||
};
|
||||
|
||||
// Fallback rápido compatible
|
||||
this.get = () => this.get_face_material(0);
|
||||
|
||||
this.reset = () => {
|
||||
for(const k in face_cache){
|
||||
if(face_cache[k]){
|
||||
face_cache[k].dispose();
|
||||
delete face_cache[k];
|
||||
}
|
||||
}
|
||||
for(const k in edge_cache){
|
||||
if(edge_cache[k]){
|
||||
edge_cache[k].dispose();
|
||||
delete edge_cache[k];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return RoomMaterialDebug;
|
||||
})();
|
||||
36
Public/ecma/Materials/Debug/StairMaterialDebug.ecma.js
Normal file
36
Public/ecma/Materials/Debug/StairMaterialDebug.ecma.js
Normal file
@ -0,0 +1,36 @@
|
||||
"use strict";
|
||||
|
||||
const StairMaterialDebug = (function(){
|
||||
|
||||
const StairMaterialDebug = function(){
|
||||
|
||||
let landing_mat = null,
|
||||
ramp_mat = null;
|
||||
|
||||
this.get_landing = () => landing_mat || (landing_mat = new THREE.MeshBasicMaterial({
|
||||
color : 0x2e7d32, // Verde oscuro / esmeralda
|
||||
transparent : true,
|
||||
opacity : 0.85
|
||||
}));
|
||||
|
||||
this.get_ramp = () => ramp_mat || (ramp_mat = new THREE.MeshBasicMaterial({
|
||||
color : 0x4caf50, // Verde lima
|
||||
transparent : true,
|
||||
opacity : 0.70
|
||||
}));
|
||||
|
||||
this.reset = () => {
|
||||
if(landing_mat){
|
||||
landing_mat.dispose();
|
||||
landing_mat = null;
|
||||
}
|
||||
if(ramp_mat){
|
||||
ramp_mat.dispose();
|
||||
ramp_mat = null;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return StairMaterialDebug;
|
||||
})();
|
||||
67
Public/ecma/Materials/Game/ElevatorMaterialGame.ecma.js
Normal file
67
Public/ecma/Materials/Game/ElevatorMaterialGame.ecma.js
Normal file
@ -0,0 +1,67 @@
|
||||
"use strict";
|
||||
|
||||
const ElevatorMaterialGame = (function(){
|
||||
|
||||
const ElevatorMaterialGame = function(hollow_bond){
|
||||
|
||||
let cabin_mat = null;
|
||||
let light_mat = null;
|
||||
|
||||
this.get_cabin_material = () => {
|
||||
if(cabin_mat) return cabin_mat;
|
||||
|
||||
const def = (hollow_bond.theme && hollow_bond.theme.elevator_cabin) ? hollow_bond.theme.elevator_cabin : {
|
||||
vertex: `
|
||||
varying vec3 vNormal;
|
||||
void main(){
|
||||
vNormal = normalize(normalMatrix * normal);
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying vec3 vNormal;
|
||||
void main(){
|
||||
gl_FragColor = vec4(vec3(0.35) + abs(vNormal.y) * 0.1, 1.0);
|
||||
}
|
||||
`
|
||||
};
|
||||
|
||||
cabin_mat = new THREE.ShaderMaterial({
|
||||
vertexShader: def.vertex,
|
||||
fragmentShader: def.fragment,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
return cabin_mat;
|
||||
};
|
||||
|
||||
this.get_light_material = () => {
|
||||
if(light_mat) return light_mat;
|
||||
|
||||
const def = (hollow_bond.theme && hollow_bond.theme.elevator_light) ? hollow_bond.theme.elevator_light : {
|
||||
vertex: `
|
||||
void main(){ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
|
||||
`,
|
||||
fragment: `
|
||||
void main(){ gl_FragColor = vec4(0.95, 0.92, 0.82, 1.0); }
|
||||
`
|
||||
};
|
||||
|
||||
light_mat = new THREE.ShaderMaterial({
|
||||
vertexShader: def.vertex,
|
||||
fragmentShader: def.fragment,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
return light_mat;
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
if(cabin_mat){ cabin_mat.dispose(); cabin_mat = null; }
|
||||
if(light_mat){ light_mat.dispose(); light_mat = null; }
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return ElevatorMaterialGame;
|
||||
})();
|
||||
47
Public/ecma/Materials/Game/FrameMaterialGame.ecma.js
Normal file
47
Public/ecma/Materials/Game/FrameMaterialGame.ecma.js
Normal file
@ -0,0 +1,47 @@
|
||||
"use strict";
|
||||
|
||||
const FrameMaterialGame = (function(){
|
||||
|
||||
const FrameMaterialGame = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
const cache = {};
|
||||
|
||||
const themes_source = (hollow_bond && hollow_bond.themes)
|
||||
? hollow_bond.themes
|
||||
: new ThemeManager(hollow_bond);
|
||||
|
||||
this.get_material = (theme_key = null) => {
|
||||
|
||||
const key = theme_key || themes_source.current_theme || "level_0";
|
||||
|
||||
if(cache[key]){
|
||||
return cache[key];
|
||||
}
|
||||
|
||||
const theme = themes_source.get(key);
|
||||
if(!theme || !theme.shaders || !theme.shaders.frame){
|
||||
throw new Error(`[FrameMaterialGame] Shader de frame no definido en tema "${key}".`);
|
||||
}
|
||||
|
||||
cache[key] = new THREE.ShaderMaterial({
|
||||
vertexShader: theme.shaders.frame.vertex,
|
||||
fragmentShader: theme.shaders.frame.fragment,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
return cache[key];
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const key in cache){
|
||||
if(cache[key] && cache[key].dispose){
|
||||
cache[key].dispose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return FrameMaterialGame;
|
||||
})();
|
||||
47
Public/ecma/Materials/Game/HoleMaterialGame.ecma.js
Normal file
47
Public/ecma/Materials/Game/HoleMaterialGame.ecma.js
Normal file
@ -0,0 +1,47 @@
|
||||
"use strict";
|
||||
|
||||
const HoleMaterialGame = (function(){
|
||||
|
||||
const HoleMaterialGame = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
const cache = {};
|
||||
|
||||
const themes_source = (hollow_bond && hollow_bond.themes)
|
||||
? hollow_bond.themes
|
||||
: new ThemeManager(hollow_bond);
|
||||
|
||||
this.get_material = (theme_key = null) => {
|
||||
|
||||
const key = theme_key || themes_source.current_theme || "level_0";
|
||||
|
||||
if(cache[key]){
|
||||
return cache[key];
|
||||
}
|
||||
|
||||
const theme = themes_source.get(key);
|
||||
const shader_def = (theme && theme.shaders && theme.shaders.hole)
|
||||
? theme.shaders.hole
|
||||
: (theme && theme.shaders && theme.shaders.walls);
|
||||
|
||||
cache[key] = new THREE.ShaderMaterial({
|
||||
vertexShader: shader_def.vertex,
|
||||
fragmentShader: shader_def.fragment,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
return cache[key];
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const key in cache){
|
||||
if(cache[key] && cache[key].dispose){
|
||||
cache[key].dispose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return HoleMaterialGame;
|
||||
})();
|
||||
49
Public/ecma/Materials/Game/RailingMaterialGame.ecma.js
Normal file
49
Public/ecma/Materials/Game/RailingMaterialGame.ecma.js
Normal file
@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
|
||||
const RailingMaterialGame = (function(){
|
||||
|
||||
const RailingMaterialGame = function(hollow_bond){
|
||||
|
||||
let material = null;
|
||||
|
||||
this.get = () => {
|
||||
if(material) return material;
|
||||
|
||||
const theme_def = (hollow_bond.theme && hollow_bond.theme.railing) ? hollow_bond.theme.railing : {
|
||||
vertex: `
|
||||
varying vec3 vNormal;
|
||||
void main(){
|
||||
vNormal = normalize(normalMatrix * normal);
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying vec3 vNormal;
|
||||
void main(){
|
||||
vec3 dark_metal = vec3(0.2, 0.19, 0.18);
|
||||
vec3 highlight = vec3(0.35, 0.33, 0.3);
|
||||
gl_FragColor = vec4(mix(dark_metal, highlight, abs(vNormal.y) * 0.3), 1.0);
|
||||
}
|
||||
`
|
||||
};
|
||||
|
||||
material = new THREE.ShaderMaterial({
|
||||
vertexShader: theme_def.vertex,
|
||||
fragmentShader: theme_def.fragment,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
return material;
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
if(material){
|
||||
material.dispose();
|
||||
material = null;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return RailingMaterialGame;
|
||||
})();
|
||||
62
Public/ecma/Materials/Game/RoomMaterialGame.ecma.js
Normal file
62
Public/ecma/Materials/Game/RoomMaterialGame.ecma.js
Normal file
@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
|
||||
const RoomMaterialGame = (function(){
|
||||
|
||||
const RoomMaterialGame = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
const cache = {};
|
||||
|
||||
const themes_source = (hollow_bond && hollow_bond.themes)
|
||||
? hollow_bond.themes
|
||||
: new ThemeManager(hollow_bond);
|
||||
|
||||
this.get_materials = (theme_key = null) => {
|
||||
|
||||
const key = theme_key || themes_source.current_theme || "level_0";
|
||||
|
||||
if(cache[key]){
|
||||
return cache[key];
|
||||
}
|
||||
|
||||
const theme = themes_source.get(key);
|
||||
if(!theme || !theme.shaders){
|
||||
throw new Error(`[RoomMaterialGame] Theme "${key}" no encontrado o sin shaders.`);
|
||||
}
|
||||
|
||||
// Cambiamos a THREE.DoubleSide para que renderice tanto interior como exterior
|
||||
const floor_mat = new THREE.ShaderMaterial({
|
||||
vertexShader: theme.shaders.floor.vertex,
|
||||
fragmentShader: theme.shaders.floor.fragment,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
const ceiling_mat = new THREE.ShaderMaterial({
|
||||
vertexShader: theme.shaders.ceiling.vertex,
|
||||
fragmentShader: theme.shaders.ceiling.fragment,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
const walls_mat = new THREE.ShaderMaterial({
|
||||
vertexShader: theme.shaders.walls.vertex,
|
||||
fragmentShader: theme.shaders.walls.fragment,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
cache[key] = [floor_mat, ceiling_mat, walls_mat];
|
||||
|
||||
return cache[key];
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
for(const key in cache){
|
||||
if(Array.isArray(cache[key])){
|
||||
cache[key].forEach(mat => mat.dispose && mat.dispose());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return RoomMaterialGame;
|
||||
})();
|
||||
47
Public/ecma/Materials/Game/StairMaterialGame.ecma.js
Normal file
47
Public/ecma/Materials/Game/StairMaterialGame.ecma.js
Normal file
@ -0,0 +1,47 @@
|
||||
"use strict";
|
||||
|
||||
const StairMaterialGame = (function(){
|
||||
|
||||
const StairMaterialGame = function(hollow_bond){
|
||||
|
||||
let material = null;
|
||||
|
||||
this.get = () => {
|
||||
if(material) return material;
|
||||
|
||||
const theme_def = (hollow_bond.theme && hollow_bond.theme.stair) ? hollow_bond.theme.stair : {
|
||||
vertex: `
|
||||
varying vec3 vNormal;
|
||||
void main(){
|
||||
vNormal = normalize(normalMatrix * normal);
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying vec3 vNormal;
|
||||
void main(){
|
||||
gl_FragColor = vec4(vec3(0.25) + abs(vNormal.y) * 0.1, 1.0);
|
||||
}
|
||||
`
|
||||
};
|
||||
|
||||
material = new THREE.ShaderMaterial({
|
||||
vertexShader: theme_def.vertex,
|
||||
fragmentShader: theme_def.fragment,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
return material;
|
||||
};
|
||||
|
||||
this.reset = () => {
|
||||
if(material){
|
||||
material.dispose();
|
||||
material = null;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return StairMaterialGame;
|
||||
})();
|
||||
96
Public/ecma/Models/ChunkModel.ecma.js
Normal file
96
Public/ecma/Models/ChunkModel.ecma.js
Normal file
@ -0,0 +1,96 @@
|
||||
"use strict";
|
||||
|
||||
const ChunkModel = (function(){
|
||||
|
||||
const ChunkModel = function(hollow_bond, inputs = {}, callback = null){
|
||||
|
||||
const self = this;
|
||||
|
||||
// Coordenadas enteras del Chunk en la rejilla macro
|
||||
this.x = Common.get_value(["x", "cx"], inputs, 0);
|
||||
this.y = Common.get_value(["y", "cy"], inputs, 0);
|
||||
this.z = Common.get_value(["z", "cz"], inputs, 0);
|
||||
|
||||
this.id = `chunk_${self.x}_${self.y}_${self.z}`;
|
||||
|
||||
// Límites espaciales absolutos: From (xf, yf, zf) -> To (xt, yt, zt)
|
||||
this.xf = this.x * hollow_bond.chunk_size;
|
||||
this.yf = this.y * hollow_bond.chunk_size;
|
||||
this.zf = this.z * hollow_bond.chunk_size;
|
||||
|
||||
this.xt = this.xf + hollow_bond.chunk_size;
|
||||
this.yt = this.yf + hollow_bond.chunk_size;
|
||||
this.zt = this.zf + hollow_bond.chunk_size;
|
||||
|
||||
// Colecciones lógicas contenidas en este Chunk
|
||||
this.rooms = {};
|
||||
this.frames = {};
|
||||
this.holes = {};
|
||||
this.stairs = {};
|
||||
this.elevators = {};
|
||||
|
||||
this.neighbors = Common.get_value("neighbors", inputs, {
|
||||
north : false,
|
||||
south : false,
|
||||
east : false,
|
||||
west : false,
|
||||
up : false,
|
||||
down : false
|
||||
});
|
||||
|
||||
// Índice de coordenadas discretas ocupadas por marcos
|
||||
// this.frames_by_position = {};
|
||||
|
||||
// Lista de cajas de ocupación (AABB) registradas
|
||||
this.occupancy = [];
|
||||
|
||||
const constructor = () => {
|
||||
hollow_bond.builders.frames.generate_seam(self);
|
||||
hollow_bond.builders.holes.generate_seam(self);
|
||||
hollow_bond.builders.holes.generate_internal(self);
|
||||
hollow_bond.builders.elevators.generate_seam(self);
|
||||
hollow_bond.builders.elevators.generate_internal(self);
|
||||
if(callback)
|
||||
hollow_bond.builders.rooms.generate_asyncronous(self, callback);
|
||||
else
|
||||
hollow_bond.builders.rooms.generate(self);
|
||||
};
|
||||
|
||||
this.is_occupied = (xf, yf, zf, xt, yt, zt) => {
|
||||
for(const box of self.occupancy)
|
||||
if(
|
||||
xf < box.xt && xt > box.xf &&
|
||||
yf < box.yt && yt > box.yf &&
|
||||
zf < box.zt && zt > box.zf
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
this.occupy = (xf, yf, zf, xt, yt, zt, type, id) => {
|
||||
self.occupancy.push({
|
||||
xf : xf,
|
||||
yf : yf,
|
||||
zf : zf,
|
||||
xt : xt,
|
||||
yt : yt,
|
||||
zt : zt,
|
||||
type : type,
|
||||
id : id
|
||||
});
|
||||
};
|
||||
|
||||
// const get_position_key = (key, x, y, z) => `${key}_${x}_${y}_${z}`;
|
||||
|
||||
// this.has_frame_at = (x, y, z) => !!self.frames_by_position[FrameModel.get_id(x, y, z)];
|
||||
|
||||
// this.get_frame_at = (x, y, z) => self.frames_by_position[FrameModel.get_id(x, y, z)] || null;
|
||||
|
||||
constructor();
|
||||
|
||||
};
|
||||
|
||||
// ChunkModel.get_position_key = (key, x, y, z) => `${key}_${x}_${y}_${z}`;
|
||||
|
||||
return ChunkModel;
|
||||
})();
|
||||
69
Public/ecma/Models/ElevatorModel.ecma.js
Normal file
69
Public/ecma/Models/ElevatorModel.ecma.js
Normal file
@ -0,0 +1,69 @@
|
||||
"use strict";
|
||||
|
||||
const ElevatorModel = (function(){
|
||||
|
||||
const DIRECTIONS = {
|
||||
north : {x : 0, z : -1},
|
||||
south : {x : 0, z : 1},
|
||||
east : {x : 1, z : 0},
|
||||
west : {x : -1, z : 0}
|
||||
};
|
||||
|
||||
const ElevatorModel = function(hollow_bond, inputs = {}){
|
||||
|
||||
const self = this;
|
||||
|
||||
this.id = ElevatorModel.get_id(inputs.x, inputs.y, inputs.z);
|
||||
this.group_id = Common.get_value("group_id", inputs, "");
|
||||
|
||||
this.x = Common.get_value("x", inputs, 0) | 0;
|
||||
this.y = Common.get_value("y", inputs, 0) | 0;
|
||||
this.z = Common.get_value("z", inputs, 0) | 0;
|
||||
this.floor = Common.get_value("floor", inputs, 0) | 0;
|
||||
|
||||
this.width = hollow_bond.elevator_size;
|
||||
this.height = hollow_bond.elevator_height;
|
||||
this.depth = hollow_bond.elevator_size;
|
||||
this.padding = hollow_bond.elevator_padding;
|
||||
|
||||
// Orientación de la puerta: "north", "south", "east", "west"
|
||||
this.facing = Common.get_value("facing", inputs, "north");
|
||||
|
||||
// Rango de pisos y habitaciones asociadas
|
||||
this.floors_from = Common.get_value("floors_from", inputs, 0) | 0;
|
||||
this.floors_count = Common.get_value("floors_count", inputs, 1) | 0;
|
||||
this.rooms = Common.get_value("rooms", inputs, []);
|
||||
this.room = null;
|
||||
|
||||
/**
|
||||
* Obtiene la habitación asociada al piso destino mediante indexado relativo.
|
||||
* @param {number} floor_number
|
||||
* @returns {RoomModel|null}
|
||||
*/
|
||||
this.get_room_at_floor = floor_number => {
|
||||
|
||||
const index = floor_number - self.floors_from;
|
||||
|
||||
return (index >= 0 && index < self.rooms.length) ? self.rooms[index] : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Devuelve las coordenadas de la casilla frente a la puerta del ascensor.
|
||||
*/
|
||||
this.get_door_threshold = () => {
|
||||
|
||||
const direction = DIRECTIONS[self.facing] || DIRECTIONS.north;
|
||||
|
||||
return {
|
||||
x : self.x + (direction.x > 0 ? self.width : (direction.x < 0 ? -1 : 0)),
|
||||
z : self.z + (direction.z > 0 ? self.depth : (direction.z < 0 ? -1 : 0)),
|
||||
facing : self.facing
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
ElevatorModel.get_id = (x, y, z) => `elevator_${x}_${y}_${z}`;
|
||||
|
||||
return ElevatorModel;
|
||||
})();
|
||||
21
Public/ecma/Models/EntityModel.ecma.js
Normal file
21
Public/ecma/Models/EntityModel.ecma.js
Normal file
@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
const EntityModel = (function(){
|
||||
|
||||
const EntityModel = function(hollow_bond, inputs){
|
||||
|
||||
const self = this;
|
||||
|
||||
this.id = Common.get_value("id", inputs, "") || hollow_bond.get_id("entity");
|
||||
this.x = Common.get_value("x", inputs, 0);
|
||||
this.y = Common.get_value("y", inputs, 0);
|
||||
this.z = Common.get_value("z", inputs, 0);
|
||||
this.width = Common.get_value("width", inputs, 0);
|
||||
this.height = Common.get_value("height", inputs, 0);
|
||||
this.depth = Common.get_value("depth", inputs, 0);
|
||||
this.speed = hollow_bond.settings.get(["speed", "entity_speed"], inputs, 6.0);
|
||||
|
||||
};
|
||||
|
||||
return EntityModel;
|
||||
})();
|
||||
47
Public/ecma/Models/FrameModel.ecma.js
Normal file
47
Public/ecma/Models/FrameModel.ecma.js
Normal file
@ -0,0 +1,47 @@
|
||||
"use strict";
|
||||
|
||||
const FrameModel = (function(){
|
||||
|
||||
const FrameModel = function(hollow_bond, inputs = {}){
|
||||
|
||||
const self = this;
|
||||
|
||||
this.x = Common.get_value("x", inputs, 0);
|
||||
this.y = Common.get_value("y", inputs, 0);
|
||||
this.z = Common.get_value("z", inputs, 0);
|
||||
|
||||
this.id = FrameModel.get_id(self.x, self.y, self.z);
|
||||
this.axis = Common.get_value("axis", inputs, "horizontal");
|
||||
this.rooms = Common.get_value("rooms", inputs, [null, null]);
|
||||
|
||||
this.width = hollow_bond.frame_width;
|
||||
this.height = hollow_bond.frame_height;
|
||||
this.padding = hollow_bond.frame_padding + hollow_bond.room_padding;
|
||||
this.size = (
|
||||
this.axis == "horizontal" ? {x : self.width, z : self.padding * 2} :
|
||||
{x : self.padding * 2, z : self.width});
|
||||
|
||||
this.connect_room = room_id => {
|
||||
if(self.rooms.includes(room_id))
|
||||
return;
|
||||
self.rooms.some((room, i) => {
|
||||
if(room === null){
|
||||
self.rooms[i] = room_id;
|
||||
return true;
|
||||
};
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
this.get_world_position = () =>({
|
||||
x : self.x,
|
||||
y : self.y * self.height,
|
||||
z : self.z
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
FrameModel.get_id = (x, y, z) => `frame_${x | 0}_${y | 0}_${z | 0}`;
|
||||
|
||||
return FrameModel;
|
||||
})();
|
||||
49
Public/ecma/Models/HoleModel.ecma.js
Normal file
49
Public/ecma/Models/HoleModel.ecma.js
Normal file
@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
|
||||
const HoleModel = (function(){
|
||||
|
||||
const HoleModel = function(hollow_bond, inputs = {}){
|
||||
|
||||
const self = this;
|
||||
|
||||
// Límites lógicos absolutos
|
||||
this.xf = Common.get_value("xf", inputs, 0) | 0;
|
||||
this.yf = Common.get_value("yf", inputs, 0) | 0; // Piso inferior
|
||||
this.zf = Common.get_value("zf", inputs, 0) | 0;
|
||||
|
||||
this.xt = Common.get_value("xt", inputs, 0) | 0;
|
||||
this.yt = Common.get_value("yt", inputs, 0) | 0; // Piso superior
|
||||
this.zt = Common.get_value("zt", inputs, 0) | 0;
|
||||
|
||||
this.width = this.xt - this.xf;
|
||||
this.height = this.yt - this.yf;
|
||||
this.depth = this.zt - this.zf;
|
||||
|
||||
this.id = HoleModel.get_id(self.xf, self.yf, self.zf, self.xt, self.yt, self.zt);
|
||||
|
||||
this.rooms = Common.get_value("rooms", inputs, []);
|
||||
this.stairs = Common.get_value("stairs", inputs, []);
|
||||
|
||||
const constructor = () => {
|
||||
hollow_bond.builders.stairs.generate(self);
|
||||
};
|
||||
|
||||
this.get_world_bounds = () => {
|
||||
return {
|
||||
x : self.xf,
|
||||
y : self.yf * hollow_bond.room_height,
|
||||
z : self.zf,
|
||||
width : self.width,
|
||||
height : self.height * hollow_bond.room_height,
|
||||
depth : self.depth
|
||||
};
|
||||
};
|
||||
|
||||
constructor();
|
||||
|
||||
};
|
||||
|
||||
HoleModel.get_id = (xf, yf, zf, xt, yt, zt) => `hole_${xf}_${yf}_${zf}_${xt}_${yt}_${zt}`;
|
||||
|
||||
return HoleModel;
|
||||
})();
|
||||
30
Public/ecma/Models/RoomModel.ecma.js
Normal file
30
Public/ecma/Models/RoomModel.ecma.js
Normal file
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
const RoomModel = (function(){
|
||||
|
||||
const RoomModel = function(hollow_bond, inputs = {}){
|
||||
|
||||
const self = this;
|
||||
|
||||
this.x = Common.get_value("x", inputs, 0);
|
||||
this.y = Common.get_value("y", inputs, 0);
|
||||
this.z = Common.get_value("z", inputs, 0);
|
||||
this.width = Common.get_value("width", inputs, 2);
|
||||
this.height = hollow_bond.settings.get(["height", "room_height"], inputs, 3);
|
||||
this.depth = Common.get_value("depth", inputs, 2);
|
||||
this.id = RoomModel.get_id(self.x, self.y, self.z, self.width, self.height, self.depth);
|
||||
this.padding = hollow_bond.settings.get(["padding", "room_padding"], inputs, .1);
|
||||
|
||||
this.frames = Common.get_value("frames", inputs, []);
|
||||
this.holes = Common.get_value("holes", inputs, []);
|
||||
this.elevators = Common.get_value("elevators", inputs, []);
|
||||
|
||||
// Orificios que atraviesan de suelo a techo la habitación: [{ x, z, width, depth }]
|
||||
this.voids = Common.get_value("voids", inputs, []);
|
||||
|
||||
};
|
||||
|
||||
RoomModel.get_id = (x, y, z, width, height, depth) => `room_${x}_${y}_${z}_${width}_${height}_${depth}`;
|
||||
|
||||
return RoomModel;
|
||||
})();
|
||||
48
Public/ecma/Models/StairsModel.ecma.js
Normal file
48
Public/ecma/Models/StairsModel.ecma.js
Normal file
@ -0,0 +1,48 @@
|
||||
"use strict";
|
||||
|
||||
const StairsModel = (function(){
|
||||
|
||||
const StairsModel = function(hollow_bond, inputs = {}){
|
||||
|
||||
const self = this;
|
||||
|
||||
this.id = Common.get_value("id", inputs, "") || hollow_bond.get_id("stairs");
|
||||
this.hole_id = Common.get_value("hole_id", inputs, "");
|
||||
|
||||
// Coordenadas lógicas de la caja total 4x1x2 o 1x4x2
|
||||
this.xf = Common.get_value("xf", inputs, 0) | 0;
|
||||
this.yf = Common.get_value("yf", inputs, 0) | 0; // Piso de salida
|
||||
this.zf = Common.get_value("zf", inputs, 0) | 0;
|
||||
|
||||
this.xt = Common.get_value("xt", inputs, 0) | 0;
|
||||
this.yt = Common.get_value("yt", inputs, 0) | 0; // Piso de llegada (yf + 2)
|
||||
this.zt = Common.get_value("zt", inputs, 0) | 0;
|
||||
|
||||
// "north", "south", "east", "west" (a qué pared interior del pozo se pega)
|
||||
this.wall = Common.get_value("wall", inputs, "north");
|
||||
|
||||
// Sentido de subida: 1 (hacia coordenadas crecientes) o -1 (hacia decrecientes)
|
||||
this.direction = Common.get_value("direction", inputs, 1);
|
||||
|
||||
this.width = this.xt - this.xf;
|
||||
this.depth = this.zt - this.zf;
|
||||
this.height = this.yt - this.yf;
|
||||
this.padding = hollow_bond.stairs_padding;
|
||||
|
||||
// Habitaciones conectadas por los descansillos inferior y superior
|
||||
this.room_from = Common.get_value("room_from", inputs, null);
|
||||
this.room_to = Common.get_value("room_to", inputs, null);
|
||||
|
||||
this.get_world_bounds = () => ({
|
||||
x : self.xf,
|
||||
y : self.yf * hollow_bond.room_height,
|
||||
z : self.zf,
|
||||
width : self.width,
|
||||
height : self.height * hollow_bond.room_height,
|
||||
depth : self.depth
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
return StairsModel;
|
||||
})();
|
||||
95
Public/ecma/Objects/Debug/ElevatorObjectDebug.ecma.js
Normal file
95
Public/ecma/Objects/Debug/ElevatorObjectDebug.ecma.js
Normal file
@ -0,0 +1,95 @@
|
||||
"use strict";
|
||||
|
||||
const ElevatorObjectDebug = (function(){
|
||||
|
||||
const ElevatorObjectDebug = function(hollow_bond, model){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
|
||||
const floor_height = hollow_bond.room_height || 3,
|
||||
size = hollow_bond.elevator_size || 2,
|
||||
height = floor_height * 0.9,
|
||||
center_x = model.x + size / 2,
|
||||
center_y = (model.y * floor_height) + (floor_height * 0.45),
|
||||
center_z = model.z + size / 2;
|
||||
|
||||
this.mesh = new THREE.Group();
|
||||
|
||||
// 1. Malla de la cabina coloreada por columna
|
||||
const column_id = model.shaft_id || model.group_id || model.column_id || `${model.x}_${model.z}`,
|
||||
cabin_geo = hollow_bond.inspector.geometries.elevator.get(),
|
||||
cabin_mat = hollow_bond.inspector.materials.elevator.get_cabin_material(column_id),
|
||||
cabin_mesh = new THREE.Mesh(cabin_geo, cabin_mat);
|
||||
// Perfilado de bordes/vértices de la cabina
|
||||
const cabin_edges_geo = new THREE.EdgesGeometry(cabin_geo),
|
||||
cabin_edge_mat = hollow_bond.inspector.materials.elevator.get_edges(),
|
||||
cabin_edges_mesh = new THREE.LineSegments(cabin_edges_geo, cabin_edge_mat);
|
||||
|
||||
this.mesh.add(cabin_mesh);
|
||||
this.mesh.add(cabin_edges_mesh);
|
||||
|
||||
// 2. Extracción de la orientación de la puerta
|
||||
// Normalizamos comprobando door, direction, orientation, side o facing
|
||||
const raw_dir = String(
|
||||
model.door ||
|
||||
model.door_direction ||
|
||||
model.direction ||
|
||||
model.orientation ||
|
||||
model.side ||
|
||||
model.facing ||
|
||||
""
|
||||
).trim().toLowerCase();
|
||||
|
||||
// const label_mat = hollow_bond.inspector.materials.elevator.get_door_label_material(model.y),
|
||||
const label_mat = hollow_bond.inspector.materials.elevator.get_door_label_material(model.floor),
|
||||
label_geo = new THREE.PlaneGeometry(size * 0.75, height * 0.75),
|
||||
label_mesh = new THREE.Mesh(label_geo, label_mat),
|
||||
offset = size / 2 + 0.02;
|
||||
|
||||
// Comprobación por cardinales o ejes relativos
|
||||
if(raw_dir === "north" || raw_dir === "-z" || raw_dir === "top" || raw_dir === "up"){
|
||||
label_mesh.position.set(0, 0, -offset);
|
||||
label_mesh.rotation.y = Math.PI;
|
||||
} else if(raw_dir === "south" || raw_dir === "+z" || raw_dir === "bottom" || raw_dir === "down"){
|
||||
label_mesh.position.set(0, 0, offset);
|
||||
label_mesh.rotation.y = 0;
|
||||
} else if(raw_dir === "east" || raw_dir === "+x" || raw_dir === "right"){
|
||||
label_mesh.position.set(offset, 0, 0);
|
||||
label_mesh.rotation.y = Math.PI / 2;
|
||||
} else if(raw_dir === "west" || raw_dir === "-x" || raw_dir === "left"){
|
||||
label_mesh.position.set(-offset, 0, 0);
|
||||
label_mesh.rotation.y = -Math.PI / 2;
|
||||
} else if(typeof model.dx === "number" || typeof model.dz === "number"){
|
||||
// Si viene guardado como vector de dirección relativo (dx, dz)
|
||||
if(model.dz < 0){
|
||||
label_mesh.position.set(0, 0, -offset);
|
||||
label_mesh.rotation.y = Math.PI;
|
||||
} else if(model.dz > 0){
|
||||
label_mesh.position.set(0, 0, offset);
|
||||
label_mesh.rotation.y = 0;
|
||||
} else if(model.dx > 0){
|
||||
label_mesh.position.set(offset, 0, 0);
|
||||
label_mesh.rotation.y = Math.PI / 2;
|
||||
} else {
|
||||
label_mesh.position.set(-offset, 0, 0);
|
||||
label_mesh.rotation.y = -Math.PI / 2;
|
||||
}
|
||||
} else {
|
||||
// Fallback por defecto si no viniera nada definido
|
||||
label_mesh.position.set(0, 0, offset);
|
||||
label_mesh.rotation.y = 0;
|
||||
}
|
||||
|
||||
this.mesh.add(label_mesh);
|
||||
this.mesh.position.set(center_x, center_y, center_z);
|
||||
|
||||
this.destroy = () => {
|
||||
if(label_geo) label_geo.dispose();
|
||||
if(cabin_edges_geo) cabin_edges_geo.dispose();
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return ElevatorObjectDebug;
|
||||
})();
|
||||
61
Public/ecma/Objects/Debug/FrameObjectDebug.ecma.js
Normal file
61
Public/ecma/Objects/Debug/FrameObjectDebug.ecma.js
Normal file
@ -0,0 +1,61 @@
|
||||
"use strict";
|
||||
|
||||
const FrameObjectDebug = (function(){
|
||||
|
||||
const FrameObjectDebug = function(hollow_bond, model){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
|
||||
const w = model.size.x,
|
||||
h = model.height,
|
||||
d = model.size.z,
|
||||
is_horizontal = model.axis === "horizontal";
|
||||
|
||||
const geometry = hollow_bond.inspector.geometries.frame.get(w, h, d),
|
||||
face_mat = hollow_bond.inspector.materials.frame.get_face_material(0),
|
||||
edge_mat = hollow_bond.inspector.materials.frame.get_edge_material(0);
|
||||
|
||||
this.mesh = new THREE.Group();
|
||||
|
||||
// 1. Malla translúcida del volumen
|
||||
this.box_mesh = new THREE.Mesh(geometry, face_mat);
|
||||
this.mesh.add(this.box_mesh);
|
||||
|
||||
// 2. Líneas perimetrales
|
||||
this.edges_geometry = new THREE.EdgesGeometry(geometry);
|
||||
this.edges_mesh = new THREE.LineSegments(this.edges_geometry, edge_mat);
|
||||
this.mesh.add(this.edges_mesh);
|
||||
|
||||
// 3. Posicionamiento centrado en la pared divisoria
|
||||
// Base de suelo según la altura de planta del juego
|
||||
const floor_h = hollow_bond.room_height || model.height,
|
||||
base_y = model.y * floor_h;
|
||||
|
||||
// Centrado: el eje de paso avanza la mitad de su ancho; el eje de grosor se centra en la línea divisoria
|
||||
const center_x = is_horizontal ? (model.x + w / 2) : model.x,
|
||||
center_y = base_y + h / 2,
|
||||
center_z = is_horizontal ? model.z : (model.z + d / 2);
|
||||
|
||||
this.mesh.position.set(center_x, center_y, center_z);
|
||||
|
||||
/**
|
||||
* Actualiza el material según la distancia al piso enfocado en el Inspector.
|
||||
* @param {number} dist - Distancia en pisos |model.y - current_floor|
|
||||
*/
|
||||
this.set_layer_distance = dist => {
|
||||
const mat_factory = hollow_bond.inspector.materials.frame;
|
||||
self.box_mesh.material = mat_factory.get_face_material(dist);
|
||||
self.edges_mesh.material = mat_factory.get_edge_material(dist);
|
||||
};
|
||||
|
||||
this.destroy = () => {
|
||||
if(self.edges_geometry){
|
||||
self.edges_geometry.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return FrameObjectDebug;
|
||||
})();
|
||||
46
Public/ecma/Objects/Debug/HoleObjectDebug.ecma.js
Normal file
46
Public/ecma/Objects/Debug/HoleObjectDebug.ecma.js
Normal file
@ -0,0 +1,46 @@
|
||||
"use strict";
|
||||
|
||||
const HoleObjectDebug = (function(){
|
||||
|
||||
const HoleObjectDebug = function(hollow_bond, model){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
|
||||
const floor_height = hollow_bond.room_height || 3,
|
||||
width = model.xt - model.xf,
|
||||
floors = model.yt - model.yf,
|
||||
depth = model.zt - model.zf,
|
||||
height = floors * floor_height;
|
||||
|
||||
this.mesh = new THREE.Group();
|
||||
|
||||
// 1. Geometría compartida y material de caras translúcidas
|
||||
const geometry = hollow_bond.inspector.geometries.hole.get(width, floors, depth),
|
||||
face_material = hollow_bond.inspector.materials.hole.get(),
|
||||
box_mesh = new THREE.Mesh(geometry, face_material);
|
||||
|
||||
this.mesh.add(box_mesh);
|
||||
|
||||
// 2. Líneas de aristas para marcar vértices y límites
|
||||
const edges_geo = new THREE.EdgesGeometry(geometry),
|
||||
edge_material = hollow_bond.inspector.materials.hole.get_edges(),
|
||||
edges_mesh = new THREE.LineSegments(edges_geo, edge_material);
|
||||
|
||||
this.mesh.add(edges_mesh);
|
||||
|
||||
// 3. Posicionamiento en el mundo
|
||||
this.mesh.position.set(
|
||||
model.xf + width / 2,
|
||||
(model.yf * floor_height) + (height / 2),
|
||||
model.zf + depth / 2
|
||||
);
|
||||
|
||||
this.destroy = () => {
|
||||
if(edges_geo) edges_geo.dispose();
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return HoleObjectDebug;
|
||||
})();
|
||||
57
Public/ecma/Objects/Debug/RoomObjectDebug.ecma.js
Normal file
57
Public/ecma/Objects/Debug/RoomObjectDebug.ecma.js
Normal file
@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
|
||||
const RoomObjectDebug = (function(){
|
||||
|
||||
const RoomObjectDebug = function(hollow_bond, model){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
|
||||
const pad = (model.padding || 0) * 2,
|
||||
w = Math.max(0.1, model.width - pad),
|
||||
h = Math.max(0.1, model.height - pad),
|
||||
d = Math.max(0.1, model.depth - pad);
|
||||
|
||||
const geometry = hollow_bond.inspector.geometries.room.get(w, h, d),
|
||||
face_mat = hollow_bond.inspector.materials.room.get_face_material(0),
|
||||
edge_mat = hollow_bond.inspector.materials.room.get_edge_material(0);
|
||||
|
||||
this.mesh = new THREE.Group();
|
||||
|
||||
// 1. Caja translúcida
|
||||
this.box_mesh = new THREE.Mesh(geometry, face_mat);
|
||||
this.mesh.add(this.box_mesh);
|
||||
|
||||
// 2. Líneas perimetrales nítidas
|
||||
this.edges_geometry = new THREE.EdgesGeometry(geometry);
|
||||
this.edges_mesh = new THREE.LineSegments(this.edges_geometry, edge_mat);
|
||||
this.mesh.add(this.edges_mesh);
|
||||
|
||||
// 3. Posicionamiento en el centro geométrico del ortoedro
|
||||
// model.y es el índice del piso en el Chunk, model.height es la altura en unidades de mundo
|
||||
this.mesh.position.set(
|
||||
model.x + model.width / 2,
|
||||
model.y + (model.height / 2),
|
||||
model.z + model.depth / 2
|
||||
);
|
||||
|
||||
/**
|
||||
* Actualiza el nivel de visibilidad y opacidad según el piso enfocado.
|
||||
* @param {number} dist - Distancia en pisos |floor - current_floor|
|
||||
*/
|
||||
this.set_layer_distance = dist => {
|
||||
const mat_factory = hollow_bond.inspector.materials.room;
|
||||
self.box_mesh.material = mat_factory.get_face_material(dist);
|
||||
self.edges_mesh.material = mat_factory.get_edge_material(dist);
|
||||
};
|
||||
|
||||
this.destroy = () => {
|
||||
if(self.edges_geometry){
|
||||
self.edges_geometry.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return RoomObjectDebug;
|
||||
})();
|
||||
70
Public/ecma/Objects/Debug/StairObjectDebug.ecma.js
Normal file
70
Public/ecma/Objects/Debug/StairObjectDebug.ecma.js
Normal file
@ -0,0 +1,70 @@
|
||||
"use strict";
|
||||
|
||||
const StairObjectDebug = (function(){
|
||||
|
||||
const StairObjectDebug = function(hollow_bond, model){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
|
||||
const floor_height = hollow_bond.room_height || 3,
|
||||
width = Math.max(1, (model.xt || (model.xf + 1)) - model.xf),
|
||||
depth = Math.max(1, (model.zt || (model.zf + 1)) - model.zf);
|
||||
|
||||
// 1. Detección de la orientación de la pared:
|
||||
// Si depth >= width, la escalera corre a lo largo de Z; de lo contrario corre a lo largo de X.
|
||||
const is_z_axis = depth >= width,
|
||||
run_axis = is_z_axis ? depth : width,
|
||||
lateral_axis = is_z_axis ? width : depth;
|
||||
|
||||
// 2. Sentido de subida: 1 (hacia coordenadas crecientes) o -1 (decrecientes)
|
||||
const direction = typeof model.direction === "number" ? model.direction : 1;
|
||||
|
||||
this.mesh = new THREE.Group();
|
||||
|
||||
// 3. Geometría y materiales desde el Inspector
|
||||
const geo_set = hollow_bond.inspector.geometries.stair.get(lateral_axis, run_axis),
|
||||
landing_mat = hollow_bond.inspector.materials.stair.get_landing(),
|
||||
ramp_mat = hollow_bond.inspector.materials.stair.get_ramp(),
|
||||
s = geo_set.specs;
|
||||
|
||||
// 4. Descansillo inferior (a cota base y = 0, extremo inicial local)
|
||||
const bottom_mesh = new THREE.Mesh(geo_set.landing_bottom, landing_mat);
|
||||
bottom_mesh.position.set(0, s.landing_thickness / 2, -run_axis / 2 + s.landing_length / 2);
|
||||
this.mesh.add(bottom_mesh);
|
||||
|
||||
// 5. Descansillo superior (enrasado en y = floor_height, extremo final local)
|
||||
const top_mesh = new THREE.Mesh(geo_set.landing_top, landing_mat);
|
||||
top_mesh.position.set(0, floor_height + (s.landing_thickness / 2), run_axis / 2 - s.landing_length / 2);
|
||||
this.mesh.add(top_mesh);
|
||||
|
||||
// 6. Rampa inclinada central
|
||||
const ramp_mesh = new THREE.Mesh(geo_set.ramp, ramp_mat);
|
||||
ramp_mesh.position.set(0, floor_height / 2 + (s.landing_thickness / 2), 0);
|
||||
ramp_mesh.rotation.x = -s.angle;
|
||||
this.mesh.add(ramp_mesh);
|
||||
|
||||
// 7. Rotación en Y según el eje y el sentido (+1 o -1)
|
||||
if(is_z_axis){
|
||||
// Subida a lo largo del eje Z
|
||||
this.mesh.rotation.y = (direction >= 0) ? 0 : Math.PI;
|
||||
} else {
|
||||
// Subida a lo largo del eje X
|
||||
this.mesh.rotation.y = (direction >= 0) ? -Math.PI / 2 : Math.PI / 2;
|
||||
}
|
||||
|
||||
// 8. Posicionado central en las coordenadas del mundo
|
||||
this.mesh.position.set(
|
||||
model.xf + width / 2,
|
||||
model.yf * floor_height,
|
||||
model.zf + depth / 2
|
||||
);
|
||||
|
||||
this.destroy = () => {
|
||||
// Geometrías y materiales compartidos se liberan centralmente en reset()
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return StairObjectDebug;
|
||||
})();
|
||||
263
Public/ecma/Objects/Game/ElevatorObjectGame.ecma.js
Normal file
263
Public/ecma/Objects/Game/ElevatorObjectGame.ecma.js
Normal file
@ -0,0 +1,263 @@
|
||||
"use strict";
|
||||
|
||||
const ElevatorObjectGame = (function(){
|
||||
|
||||
const STATES = {
|
||||
IDLE_CLOSED: "idle_closed",
|
||||
WAITING_ARRIVAL: "waiting_arrival", // Pequeña espera tras pulsar llamada
|
||||
OPENING: "opening",
|
||||
IDLE_OPEN: "idle_open", // Abierto esperando entrada
|
||||
CLOSING: "closing",
|
||||
TRANSIT: "transit" // Viaje a oscuras
|
||||
};
|
||||
|
||||
const ROTATIONS = {
|
||||
south: 0,
|
||||
north: Math.PI,
|
||||
east: Math.PI * 0.5,
|
||||
west: -Math.PI * 0.5
|
||||
};
|
||||
|
||||
// Función de interpolación suave (aceleración y deceleración)
|
||||
const smoothstep = t => t * t * (3 - 2 * t);
|
||||
|
||||
const ElevatorObjectGame = function(hollow_bond, model, material_factory, geometry_factory){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
this.state = STATES.IDLE_CLOSED;
|
||||
|
||||
this.door_openness = 0.0; // 0 = cerrada, 1 = abierta
|
||||
this.timer = 0.0;
|
||||
const DOOR_TRANSITION_TIME = 1.25; // Duración de apertura/cierre en segundos
|
||||
|
||||
const geom_fac = geometry_factory
|
||||
|| (hollow_bond.geometries && hollow_bond.geometries.elevator)
|
||||
|| new ElevatorGeometryGame(hollow_bond);
|
||||
|
||||
const mat_fac = material_factory
|
||||
|| (hollow_bond.materials && hollow_bond.materials.elevator)
|
||||
|| new ElevatorMaterialGame(hollow_bond);
|
||||
|
||||
// 1. Contenedor Raíz
|
||||
this.mesh = new THREE.Group();
|
||||
const base_x = model.x + (model.width * 0.5);
|
||||
const base_y = model.y * hollow_bond.room_height;
|
||||
const base_z = model.z + (model.depth * 0.5);
|
||||
this.mesh.position.set(base_x, base_y, base_z);
|
||||
this.mesh.rotation.y = ROTATIONS[model.facing] || 0;
|
||||
|
||||
// 2. Malla de la Cabina
|
||||
const cabin_geo = geom_fac.get_cabin_geometry(model);
|
||||
const cabin_mat = mat_fac.get_cabin_material();
|
||||
this.cabin_mesh = new THREE.Mesh(cabin_geo, cabin_mat);
|
||||
this.mesh.add(this.cabin_mesh);
|
||||
|
||||
// 3. Plafón Fluorescente
|
||||
const light_geo = geom_fac.get_light_geometry(model);
|
||||
this.light_mat = mat_fac.get_light_material();
|
||||
this.light_mesh = new THREE.Mesh(light_geo, this.light_mat);
|
||||
this.mesh.add(this.light_mesh);
|
||||
|
||||
// 4. Puertas Correderas (Hojas Izquierda y Derecha)
|
||||
const door_geo = geom_fac.get_door_leaf_geometry(model);
|
||||
const hd = (model.depth || hollow_bond.elevator_size || 2.0) * 0.5;
|
||||
|
||||
this.door_left = new THREE.Mesh(door_geo, cabin_mat);
|
||||
this.door_left.position.set(-0.28, 0, hd - 0.04);
|
||||
this.mesh.add(this.door_left);
|
||||
|
||||
this.door_right = new THREE.Mesh(door_geo, cabin_mat);
|
||||
this.door_right.position.set(0.28, 0, hd - 0.04);
|
||||
this.mesh.add(this.door_right);
|
||||
|
||||
// 5. Marco Frontal
|
||||
const frame_geom_fac = (hollow_bond.geometries && hollow_bond.geometries.elevator_frame)
|
||||
|| new ElevatorFrameGeometryGame(hollow_bond);
|
||||
|
||||
const frame_geo = frame_geom_fac.get_for_elevator(model);
|
||||
this.frame_mesh = new THREE.Mesh(frame_geo, [cabin_mat, this.light_mat]);
|
||||
this.frame_mesh.position.set(0, 0, hd);
|
||||
this.mesh.add(this.frame_mesh);
|
||||
|
||||
// 6. Collider físico dinámico
|
||||
this.collider = new ElevatorCollider(hollow_bond, model, self);
|
||||
|
||||
/**
|
||||
* Comprueba si el rayo de la cámara apunta al ascensor (marco, puertas o botón)
|
||||
*/
|
||||
this.try_click_button = raycaster => {
|
||||
const hits = raycaster.intersectObject(self.mesh, true);
|
||||
|
||||
// 1. Detección por rayo directo a cualquier parte de la cabina o puertas
|
||||
if(hits.length > 0 && hits[0].distance <= 3.2){
|
||||
return self.trigger_interaction();
|
||||
}
|
||||
|
||||
// 2. Fallback por proximidad: si estás frente a la puerta (< 2.2m) y haces click
|
||||
const dungeon = hollow_bond.dungeon;
|
||||
if(dungeon && dungeon.camera){
|
||||
const p_pos = dungeon.camera.position;
|
||||
const dist_sq = (p_pos.x - base_x) * (p_pos.x - base_x) + (p_pos.z - base_z) * (p_pos.z - base_z);
|
||||
const feet_y = p_pos.y - 1.75;
|
||||
|
||||
if(dist_sq <= (2.4 * 2.4) && Math.abs(feet_y - base_y) < 1.0){
|
||||
return self.trigger_interaction();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transición de estados al pulsar el botón
|
||||
*/
|
||||
this.trigger_interaction = () => {
|
||||
const dungeon = hollow_bond.dungeon;
|
||||
if(!dungeon || !dungeon.camera) return false;
|
||||
|
||||
const is_in = self.collider.is_inside(dungeon.camera.position.x, dungeon.camera.position.z);
|
||||
|
||||
// Botón exterior (llamada)
|
||||
if(!is_in && self.state === STATES.IDLE_CLOSED){
|
||||
self.state = STATES.WAITING_ARRIVAL;
|
||||
self.timer = 1.5; // Breve espera con sonido/parpadeo de llegada
|
||||
return true;
|
||||
}
|
||||
|
||||
// Botón interior (cierre y despegue)
|
||||
if(is_in && (self.state === STATES.IDLE_OPEN || self.state === STATES.IDLE_CLOSED)){
|
||||
self.state = STATES.CLOSING;
|
||||
self.timer = 0.0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const execute_transit = () => {
|
||||
const map = hollow_bond.map;
|
||||
if(!map || !map.elevators) return;
|
||||
|
||||
const group_cabins = [];
|
||||
for(const eid in map.elevators){
|
||||
const el = map.elevators[eid];
|
||||
if(el.group_id === model.group_id){
|
||||
group_cabins.push(el);
|
||||
}
|
||||
}
|
||||
if(group_cabins.length <= 1) return;
|
||||
|
||||
group_cabins.sort((a, b) => a.floor - b.floor);
|
||||
|
||||
const cur_index = group_cabins.findIndex(c => c.id === model.id);
|
||||
const next_index = (cur_index + 1) % group_cabins.length;
|
||||
const target_model = group_cabins[next_index];
|
||||
|
||||
const dungeon = hollow_bond.dungeon;
|
||||
if(!dungeon || !dungeon.camera) return;
|
||||
|
||||
const target_cx = target_model.x + (target_model.width * 0.5);
|
||||
const target_cz = target_model.z + (target_model.depth * 0.5);
|
||||
const target_by = target_model.y * hollow_bond.room_height;
|
||||
const eye_h = (hollow_bond.controls && hollow_bond.controls.game) ? hollow_bond.controls.game.current_height : 1.75;
|
||||
|
||||
dungeon.camera.position.set(target_cx, target_by + eye_h, target_cz);
|
||||
|
||||
const angle_delta = (ROTATIONS[target_model.facing] || 0) - (ROTATIONS[model.facing] || 0);
|
||||
if(hollow_bond.controls && hollow_bond.controls.game){
|
||||
hollow_bond.controls.game.yaw = (hollow_bond.controls.game.yaw || 0) + angle_delta;
|
||||
}
|
||||
|
||||
dungeon.check_room_transition && dungeon.check_room_transition();
|
||||
dungeon.update_rooms_streaming && dungeon.update_rooms_streaming();
|
||||
|
||||
const target_obj = dungeon.objects.elevator ? dungeon.objects.elevator[target_model.id] : null;
|
||||
if(target_obj){
|
||||
target_obj.state = STATES.OPENING;
|
||||
target_obj.timer = 0.0;
|
||||
}
|
||||
|
||||
self.state = STATES.IDLE_CLOSED;
|
||||
self.door_openness = 0.0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Actualización continua de estados y animación con Smoothstep
|
||||
*/
|
||||
this.update = delta => {
|
||||
switch(self.state){
|
||||
case STATES.WAITING_ARRIVAL:
|
||||
self.timer -= delta;
|
||||
if(self.timer <= 0){
|
||||
self.state = STATES.OPENING;
|
||||
self.timer = 0.0;
|
||||
}
|
||||
break;
|
||||
|
||||
case STATES.OPENING:
|
||||
self.timer += delta;
|
||||
const open_t = Math.min(1.0, self.timer / DOOR_TRANSITION_TIME);
|
||||
self.door_openness = smoothstep(open_t);
|
||||
|
||||
if(open_t >= 1.0){
|
||||
self.state = STATES.IDLE_OPEN;
|
||||
self.timer = 5.0; // Se queda abierto 5 segundos
|
||||
}
|
||||
break;
|
||||
|
||||
case STATES.IDLE_OPEN:
|
||||
self.timer -= delta;
|
||||
if(self.timer <= 0){
|
||||
self.state = STATES.CLOSING;
|
||||
self.timer = 0.0;
|
||||
}
|
||||
break;
|
||||
|
||||
case STATES.CLOSING:
|
||||
self.timer += delta;
|
||||
const close_t = Math.min(1.0, self.timer / DOOR_TRANSITION_TIME);
|
||||
self.door_openness = 1.0 - smoothstep(close_t);
|
||||
|
||||
if(close_t >= 1.0){
|
||||
self.door_openness = 0.0;
|
||||
const dungeon = hollow_bond.dungeon;
|
||||
const is_in = dungeon && dungeon.camera && self.collider.is_inside(dungeon.camera.position.x, dungeon.camera.position.z);
|
||||
|
||||
if(is_in){
|
||||
self.state = STATES.TRANSIT;
|
||||
self.timer = 2.4;
|
||||
} else {
|
||||
self.state = STATES.IDLE_CLOSED;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case STATES.TRANSIT:
|
||||
self.timer -= delta;
|
||||
if(self.light_mesh){
|
||||
self.light_mesh.visible = (Math.random() > 0.07);
|
||||
}
|
||||
if(self.timer <= 0){
|
||||
if(self.light_mesh) self.light_mesh.visible = true;
|
||||
execute_transit();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Desplazamiento visual de las hojas con holgura en el carril
|
||||
const slide = self.door_openness * 0.52;
|
||||
this.door_left.position.x = -0.28 - slide;
|
||||
this.door_right.position.x = 0.28 + slide;
|
||||
};
|
||||
|
||||
this.destroy = () => {
|
||||
if(self.mesh && self.mesh.parent){
|
||||
self.mesh.parent.remove(self.mesh);
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return ElevatorObjectGame;
|
||||
})();
|
||||
46
Public/ecma/Objects/Game/FrameObjectGame.ecma.js
Normal file
46
Public/ecma/Objects/Game/FrameObjectGame.ecma.js
Normal file
@ -0,0 +1,46 @@
|
||||
"use strict";
|
||||
|
||||
const FrameObjectGame = (function(){
|
||||
|
||||
const FrameObjectGame = function(hollow_bond, model, geometry_factory, material_factory){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
|
||||
const geom_fac = geometry_factory
|
||||
|| (hollow_bond.dungeon && hollow_bond.dungeon.geometries && hollow_bond.dungeon.geometries.frame)
|
||||
|| new FrameGeometryGame(hollow_bond);
|
||||
|
||||
const mat_fac = material_factory
|
||||
|| (hollow_bond.dungeon && hollow_bond.dungeon.materials && hollow_bond.dungeon.materials.frame)
|
||||
|| new FrameMaterialGame(hollow_bond);
|
||||
|
||||
const is_horiz = model.axis === "horizontal";
|
||||
const pass_w = model.width;
|
||||
const pad_y = hollow_bond.room_padding || 0;
|
||||
const wall_depth = pad_y * 2;
|
||||
const h = model.height;
|
||||
|
||||
const geometry = geom_fac.get(pass_w, h, wall_depth, is_horiz);
|
||||
const material = mat_fac.get_material();
|
||||
|
||||
this.mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const floor_h = hollow_bond.room_height || 3;
|
||||
// Misma cota base de suelo que la habitación
|
||||
const base_y = (model.y * floor_h) + pad_y;
|
||||
|
||||
const center_x = is_horiz ? (model.x + model.width / 2) : model.x;
|
||||
const center_y = base_y + h / 2;
|
||||
const center_z = is_horiz ? model.z : (model.z + model.width / 2);
|
||||
|
||||
this.mesh.position.set(center_x, center_y, center_z);
|
||||
|
||||
this.collider = new FrameCollider(hollow_bond, model);
|
||||
|
||||
this.destroy = () => {};
|
||||
|
||||
};
|
||||
|
||||
return FrameObjectGame;
|
||||
})();
|
||||
40
Public/ecma/Objects/Game/HoleObjectGame.ecma.js
Normal file
40
Public/ecma/Objects/Game/HoleObjectGame.ecma.js
Normal file
@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
|
||||
const HoleObjectGame = (function(){
|
||||
|
||||
const HoleObjectGame = function(hollow_bond, model, geometry_factory, material_factory){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
|
||||
const geom_fac = geometry_factory
|
||||
|| (hollow_bond.dungeon && hollow_bond.dungeon.geometries && hollow_bond.dungeon.geometries.hole)
|
||||
|| new HoleGeometryGame(hollow_bond);
|
||||
|
||||
const mat_fac = material_factory
|
||||
|| (hollow_bond.dungeon && hollow_bond.dungeon.materials && hollow_bond.dungeon.materials.hole)
|
||||
|| new HoleMaterialGame(hollow_bond);
|
||||
|
||||
const width = model.xt - model.xf;
|
||||
const depth = model.zt - model.zf;
|
||||
const floor_h = hollow_bond.room_height || 3;
|
||||
const pad_y = hollow_bond.room_padding || 0;
|
||||
|
||||
const geometry = geom_fac.get(width, depth, model.yf, model.yt, floor_h, pad_y);
|
||||
const material = mat_fac.get_material();
|
||||
|
||||
this.mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
// Posicionado en el centro del pozo en X y Z; el eje Y ya viene calculado de forma absoluta
|
||||
this.mesh.position.set(
|
||||
model.xf + width / 2,
|
||||
0,
|
||||
model.zf + depth / 2
|
||||
);
|
||||
|
||||
this.destroy = () => {};
|
||||
|
||||
};
|
||||
|
||||
return HoleObjectGame;
|
||||
})();
|
||||
46
Public/ecma/Objects/Game/RailingObjectGame.ecma.js
Normal file
46
Public/ecma/Objects/Game/RailingObjectGame.ecma.js
Normal file
@ -0,0 +1,46 @@
|
||||
"use strict";
|
||||
|
||||
const RailingObjectGame = (function(){
|
||||
|
||||
const RailingObjectGame = function(hollow_bond, segment, material, room_model){
|
||||
|
||||
const self = this;
|
||||
this.segment = segment;
|
||||
this.collider = new RailingCollider(hollow_bond, segment);
|
||||
|
||||
const world_cx = (segment.xf + segment.xt) * 0.5;
|
||||
const world_cz = (segment.zf + segment.zt) * 0.5;
|
||||
|
||||
const room_cx = room_model.x + (room_model.width * 0.5);
|
||||
const room_cz = room_model.z + (room_model.depth * 0.5);
|
||||
|
||||
const local_x = world_cx - room_cx;
|
||||
const local_y = -room_model.height * 0.5;
|
||||
const local_z = world_cz - room_cz;
|
||||
|
||||
const geom_fac = (hollow_bond.geometries && hollow_bond.geometries.railing)
|
||||
|| (hollow_bond.dungeon && hollow_bond.dungeon.geometries && hollow_bond.dungeon.geometries.railing)
|
||||
|| new RailingGeometryGame(hollow_bond);
|
||||
|
||||
const mat_fac = (hollow_bond.materials && hollow_bond.materials.railing)
|
||||
|| (hollow_bond.dungeon && hollow_bond.dungeon.materials && hollow_bond.dungeon.materials.railing)
|
||||
|| new RailingMaterialGame(hollow_bond);
|
||||
|
||||
// Si material es null o undefined, obtenemos uno válido directamente
|
||||
const resolved_material = material || mat_fac.get();
|
||||
|
||||
const geometry = geom_fac.get_for_segment(segment);
|
||||
|
||||
this.mesh = new THREE.Mesh(geometry, resolved_material);
|
||||
this.mesh.position.set(local_x, local_y, local_z);
|
||||
|
||||
this.destroy = () => {
|
||||
if(self.mesh && self.mesh.parent){
|
||||
self.mesh.parent.remove(self.mesh);
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return RailingObjectGame;
|
||||
})();
|
||||
237
Public/ecma/Objects/Game/RoomObjectGame.ecma.js
Normal file
237
Public/ecma/Objects/Game/RoomObjectGame.ecma.js
Normal file
@ -0,0 +1,237 @@
|
||||
"use strict";
|
||||
|
||||
const RoomObjectGame = (function(){
|
||||
|
||||
const RoomObjectGame = function(hollow_bond, model, geometry_factory, material_factory){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
|
||||
const geom_fac = geometry_factory
|
||||
|| (hollow_bond.dungeon && hollow_bond.dungeon.geometries && hollow_bond.dungeon.geometries.room)
|
||||
|| new RoomGeometryGame(hollow_bond);
|
||||
|
||||
const mat_fac = material_factory
|
||||
|| (hollow_bond.dungeon && hollow_bond.dungeon.materials && hollow_bond.dungeon.materials.room)
|
||||
|| new RoomMaterialGame(hollow_bond);
|
||||
|
||||
// 1. Recuperamos marcos de esta sala
|
||||
const frame_models = [];
|
||||
if(model.frames && hollow_bond.map && hollow_bond.map.frames){
|
||||
for(const fid of model.frames){
|
||||
const f = hollow_bond.map.frames[fid];
|
||||
if(f) frame_models.push(f);
|
||||
}
|
||||
}
|
||||
|
||||
// 1.1 Detectamos ascensores que lindan con esta sala para abrir el corte en la pared
|
||||
const py = Math.round(model.y / model.height);
|
||||
const map_elevators = (hollow_bond.map && hollow_bond.map.elevators) || {};
|
||||
|
||||
const door_w = hollow_bond.elevator_door_width || 1.1;
|
||||
const cut_w = door_w + 0.08; // 1.18m (4cm de holgura a cada lado para que el marco solape)
|
||||
const cut_h = 2.10; // 2.10m de altura para que el dintel cubra la parte superior
|
||||
|
||||
for(const eid in map_elevators){
|
||||
const el = map_elevators[eid];
|
||||
if(el.y !== py) continue;
|
||||
|
||||
const ex1 = el.x, ex2 = el.x + el.width;
|
||||
const ez1 = el.z, ez2 = el.z + el.depth;
|
||||
const rx1 = model.x, rx2 = model.x + model.width;
|
||||
const rz1 = model.z, rz2 = model.z + model.depth;
|
||||
|
||||
let touches = false;
|
||||
let axis = "horizontal";
|
||||
let open_x = 0, open_z = 0;
|
||||
|
||||
// Norte: ascensor mira al norte, habitación está al norte (pared sur de la sala)
|
||||
if(el.facing === "north" && Math.abs(rz2 - ez1) < 0.2 && ex2 > rx1 && ex1 < rx2){
|
||||
touches = true;
|
||||
axis = "horizontal";
|
||||
open_x = (ex1 + ex2) * 0.5 - cut_w * 0.5;
|
||||
open_z = rz2;
|
||||
}
|
||||
// Sur: ascensor mira al sur, habitación está al sur (pared norte de la sala)
|
||||
else if(el.facing === "south" && Math.abs(rz1 - ez2) < 0.2 && ex2 > rx1 && ex1 < rx2){
|
||||
touches = true;
|
||||
axis = "horizontal";
|
||||
open_x = (ex1 + ex2) * 0.5 - cut_w * 0.5;
|
||||
open_z = rz1;
|
||||
}
|
||||
// Oeste: ascensor mira al oeste, habitación está al oeste (pared este de la sala)
|
||||
else if(el.facing === "west" && Math.abs(rx2 - ex1) < 0.2 && ez2 > rz1 && ez1 < rz2){
|
||||
touches = true;
|
||||
axis = "vertical";
|
||||
open_x = rx2;
|
||||
open_z = (ez1 + ez2) * 0.5 - cut_w * 0.5;
|
||||
}
|
||||
// Este: ascensor mira al este, habitación está al este (pared oeste de la sala)
|
||||
else if(el.facing === "east" && Math.abs(rx1 - ex2) < 0.2 && ez2 > rz1 && ez1 < rz2){
|
||||
touches = true;
|
||||
axis = "vertical";
|
||||
open_x = rx1;
|
||||
open_z = (ez1 + ez2) * 0.5 - cut_w * 0.5;
|
||||
}
|
||||
|
||||
if(touches){
|
||||
model.elevators = model.elevators || [];
|
||||
if(!model.elevators.includes(el.id)) model.elevators.push(el.id);
|
||||
|
||||
frame_models.push({
|
||||
id: `elev_cut_${el.id}`,
|
||||
axis: axis,
|
||||
x: open_x,
|
||||
z: open_z,
|
||||
width: cut_w,
|
||||
height: cut_h
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Recuperamos huecos verticales de esta sala
|
||||
const hole_models = [];
|
||||
if(model.holes && hollow_bond.map && hollow_bond.map.holes){
|
||||
for(const hid of model.holes){
|
||||
const h = hollow_bond.map.holes[hid];
|
||||
if(h) hole_models.push(h);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Geometría con vanos de pared y perforaciones en suelo/techo
|
||||
const geometry = geom_fac.get_for_room(model, frame_models, hole_models);
|
||||
|
||||
// 4. Materiales del tema
|
||||
const materials = mat_fac.get_materials();
|
||||
|
||||
this.mesh = new THREE.Mesh(geometry, materials);
|
||||
|
||||
const center_x = model.x + model.width / 2;
|
||||
const center_y = model.y + (model.height / 2);
|
||||
const center_z = model.z + model.depth / 2;
|
||||
|
||||
this.mesh.position.set(center_x, center_y, center_z);
|
||||
|
||||
// 5. Generación de Barandillas en los huecos con caída inferior
|
||||
this.railings = [];
|
||||
|
||||
const build_railings = () => {
|
||||
const py = Math.round(model.y / model.height);
|
||||
// Solo huecos que perforan hacia abajo en este piso
|
||||
const active_holes = hole_models.filter(h => py > h.yf && py < h.yt);
|
||||
if(active_holes.length === 0) return;
|
||||
|
||||
const railing_mat = (hollow_bond.materials && hollow_bond.materials.railing)
|
||||
? hollow_bond.materials.railing.get()
|
||||
: (hollow_bond.dungeon && hollow_bond.dungeon.materials && hollow_bond.dungeon.materials.railing
|
||||
? hollow_bond.dungeon.materials.railing.get()
|
||||
: null);
|
||||
|
||||
const landing_size = hollow_bond.stairs_landing_size || 1.0;
|
||||
|
||||
const subtract_1d = (min, max, cuts) => {
|
||||
let segments = [{ start: min, end: max }];
|
||||
for(const c of cuts){
|
||||
const next = [];
|
||||
for(const s of segments){
|
||||
if(c.end <= s.start || c.start >= s.end){
|
||||
next.push(s);
|
||||
continue;
|
||||
}
|
||||
if(c.start > s.start) next.push({ start: s.start, end: c.start });
|
||||
if(c.end < s.end) next.push({ start: c.end, end: s.end });
|
||||
}
|
||||
segments = next;
|
||||
}
|
||||
return segments.filter(s => (s.end - s.start) > 0.05);
|
||||
};
|
||||
|
||||
for(const hole of active_holes){
|
||||
const hx1 = Math.max(model.x, hole.xf);
|
||||
const hx2 = Math.min(model.x + model.width, hole.xt);
|
||||
const hz1 = Math.max(model.z, hole.zf);
|
||||
const hz2 = Math.min(model.z + model.depth, hole.zt);
|
||||
|
||||
if(hx2 <= hx1 || hz2 <= hz1) continue;
|
||||
|
||||
// Identificar descansillos de escaleras
|
||||
const cuts = { north: [], south: [], west: [], east: [] };
|
||||
|
||||
for(const stair of (hole.stairs || [])){
|
||||
const stair_top_py = stair.yf + 1;
|
||||
let at_floor = (stair.yf === py) ? "bottom" : (stair_top_py === py ? "top" : null);
|
||||
if(!at_floor) continue;
|
||||
|
||||
const is_start = (at_floor === "bottom" && stair.direction === 1) ||
|
||||
(at_floor === "top" && stair.direction === -1);
|
||||
|
||||
if(stair.wall === "north" || stair.wall === "south"){
|
||||
const lx1 = is_start ? stair.xf : (stair.xt - landing_size);
|
||||
const lx2 = lx1 + landing_size;
|
||||
cuts[stair.wall].push({ start: lx1, end: lx2 });
|
||||
|
||||
// Esquinas: liberar el lado perpendicular si toca el lateral
|
||||
if(Math.abs(lx1 - hole.xf) < 0.05) cuts.west.push({ start: hole.zf, end: hole.zf + landing_size });
|
||||
if(Math.abs(lx2 - hole.xt) < 0.05) cuts.east.push({ start: hole.zf, end: hole.zf + landing_size });
|
||||
} else {
|
||||
const lz1 = is_start ? stair.zf : (stair.zt - landing_size);
|
||||
const lz2 = lz1 + landing_size;
|
||||
cuts[stair.wall].push({ start: lz1, end: lz2 });
|
||||
|
||||
if(Math.abs(lz1 - hole.zf) < 0.05) cuts.north.push({ start: (stair.wall === "west" ? hole.xf : hole.xt - landing_size), end: (stair.wall === "west" ? hole.xf + landing_size : hole.xt) });
|
||||
if(Math.abs(lz2 - hole.zt) < 0.05) cuts.south.push({ start: (stair.wall === "west" ? hole.xf : hole.xt - landing_size), end: (stair.wall === "west" ? hole.xf + landing_size : hole.xt) });
|
||||
}
|
||||
}
|
||||
|
||||
// Generar los 4 lados perimetrales
|
||||
const add_side = (side, min, max, fixed, cuts_arr) => {
|
||||
const valid_segs = subtract_1d(min, max, cuts_arr);
|
||||
for(const s of valid_segs){
|
||||
const is_horiz = (side === "north" || side === "south");
|
||||
const seg_data = {
|
||||
side: side,
|
||||
y: model.y, // Cota de mundo
|
||||
height: 1.0,
|
||||
thickness: 0.08,
|
||||
xf: is_horiz ? s.start : fixed,
|
||||
xt: is_horiz ? s.end : fixed,
|
||||
zf: is_horiz ? fixed : s.start,
|
||||
zt: is_horiz ? fixed : s.end
|
||||
};
|
||||
|
||||
const rail_obj = new RailingObjectGame(hollow_bond, seg_data, railing_mat, model);
|
||||
self.railings.push(rail_obj);
|
||||
self.mesh.add(rail_obj.mesh);
|
||||
}
|
||||
};
|
||||
|
||||
if(hole.zf >= model.z) add_side("north", hx1, hx2, hole.zf, cuts.north);
|
||||
if(hole.zt <= (model.z + model.depth)) add_side("south", hx1, hx2, hole.zt, cuts.south);
|
||||
if(hole.xf >= model.x) add_side("west", hz1, hz2, hole.xf, cuts.west);
|
||||
if(hole.xt <= (model.x + model.width)) add_side("east", hz1, hz2, hole.xt, cuts.east);
|
||||
}
|
||||
};
|
||||
|
||||
build_railings();
|
||||
|
||||
// 6. Collider con caídas y pasos libres
|
||||
this.collider = new RoomCollider(hollow_bond, model, frame_models, hole_models);
|
||||
// Vinculamos los colliders de las barandillas directamente a la sala
|
||||
this.collider.railings = this.railings.map(r => r.collider);
|
||||
|
||||
this.destroy = () => {
|
||||
if(self.railings){
|
||||
for(let i = 0; i < self.railings.length; i++){
|
||||
self.railings[i].destroy();
|
||||
}
|
||||
self.railings = [];
|
||||
}
|
||||
if(self.mesh){
|
||||
self.mesh.geometry.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return RoomObjectGame;
|
||||
})();
|
||||
44
Public/ecma/Objects/Game/StairObjectGame.ecma.js
Normal file
44
Public/ecma/Objects/Game/StairObjectGame.ecma.js
Normal file
@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
|
||||
const StairObjectGame = (function(){
|
||||
|
||||
const StairObjectGame = function(hollow_bond, model, material_factory, geometry_factory){
|
||||
|
||||
const self = this;
|
||||
this.model = model;
|
||||
this.collider = new StairCollider(hollow_bond, model);
|
||||
|
||||
const geom_fac = geometry_factory
|
||||
|| (hollow_bond.dungeon && hollow_bond.dungeon.geometries && hollow_bond.dungeon.geometries.stair)
|
||||
|| new StairGeometryGame(hollow_bond);
|
||||
|
||||
const mat_fac = material_factory
|
||||
|| (hollow_bond.dungeon && hollow_bond.dungeon.materials && hollow_bond.dungeon.materials.stair)
|
||||
|| new StairMaterialGame(hollow_bond);
|
||||
|
||||
const hole = hollow_bond.map && hollow_bond.map.holes ? hollow_bond.map.holes[model.hole_id] : null;
|
||||
const geometry = geom_fac.get_for_stair(model, hole);
|
||||
const material = mat_fac.get();
|
||||
|
||||
this.mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const bounds = model.get_world_bounds();
|
||||
const is_x = (model.wall === "north" || model.wall === "south");
|
||||
|
||||
// El punto de anclaje de la geometría se construyó desde [0, 0, 0] hacia adelante
|
||||
const pos_x = is_x ? bounds.x : (bounds.x + bounds.width * 0.5);
|
||||
const pos_y = bounds.y;
|
||||
const pos_z = is_x ? (bounds.z + bounds.depth * 0.5) : bounds.z;
|
||||
|
||||
this.mesh.position.set(pos_x, pos_y, pos_z);
|
||||
|
||||
this.destroy = () => {
|
||||
if(self.mesh && self.mesh.parent){
|
||||
self.mesh.parent.remove(self.mesh);
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return StairObjectGame;
|
||||
})();
|
||||
237
Public/ecma/Themes/Level0Theme.ecma.js
Normal file
237
Public/ecma/Themes/Level0Theme.ecma.js
Normal file
@ -0,0 +1,237 @@
|
||||
"use strict";
|
||||
|
||||
const Level0Theme = {
|
||||
key : "level_0",
|
||||
name: "Level 0 - The Lobby",
|
||||
shaders: {
|
||||
floor: {
|
||||
vertex: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying vec2 vUv;
|
||||
|
||||
float hash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 col_base = vec3(0.55, 0.48, 0.32);
|
||||
vec3 col_dark = vec3(0.42, 0.36, 0.24);
|
||||
|
||||
// Patrón tenue de moqueta húmeda
|
||||
float grain = hash(floor(vUv * 50.0)) * 0.15;
|
||||
vec3 color = mix(col_base, col_dark, grain);
|
||||
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
`
|
||||
},
|
||||
ceiling: {
|
||||
vertex: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying vec2 vUv;
|
||||
|
||||
float hash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 panel_uv = fract(vUv);
|
||||
float border = step(0.02, panel_uv.x) * step(panel_uv.x, 0.98) *
|
||||
step(0.02, panel_uv.y) * step(panel_uv.y, 0.98);
|
||||
|
||||
vec3 plate = vec3(0.82, 0.81, 0.77);
|
||||
vec3 grid = vec3(0.35, 0.35, 0.35);
|
||||
|
||||
float speck = hash(floor(panel_uv * 100.0)) * 0.05;
|
||||
plate -= speck;
|
||||
|
||||
gl_FragColor = vec4(mix(grid, plate, border), 1.0);
|
||||
}
|
||||
`
|
||||
},
|
||||
walls: {
|
||||
vertex: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying vec2 vUv;
|
||||
|
||||
float hash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 col_yellow = vec3(0.76, 0.70, 0.42);
|
||||
vec3 col_dark = vec3(0.68, 0.62, 0.36);
|
||||
|
||||
float stripe = sin(vUv.x * 31.4159) * 0.5 + 0.5;
|
||||
vec3 color = mix(col_yellow, col_dark, stripe * 0.12);
|
||||
|
||||
// Zócalo inferior
|
||||
if(vUv.y < 0.12) {
|
||||
color = vec3(0.25, 0.22, 0.18);
|
||||
}
|
||||
|
||||
float grain = hash(floor(vUv * 140.0)) * 0.05;
|
||||
color -= grain;
|
||||
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
`
|
||||
},
|
||||
frame: {
|
||||
vertex: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying vec2 vUv;
|
||||
|
||||
float hash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Tono marco madera barnizada oscura / metal marrón de oficina
|
||||
vec3 col_base = vec3(0.24, 0.18, 0.14);
|
||||
vec3 col_grain = vec3(0.18, 0.13, 0.10);
|
||||
|
||||
// Veteado suave alineado al marco
|
||||
float grain = hash(floor(vUv * vec2(15.0, 180.0))) * 0.12;
|
||||
vec3 color = mix(col_base, col_grain, grain);
|
||||
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
`
|
||||
},
|
||||
hole: {
|
||||
vertex: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying vec2 vUv;
|
||||
|
||||
float hash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Hormigón oscuro / forjado de obra
|
||||
vec3 col_base = vec3(0.22, 0.20, 0.18);
|
||||
vec3 col_grain = vec3(0.14, 0.13, 0.12);
|
||||
|
||||
float grain = hash(floor(vUv * 60.0)) * 0.25;
|
||||
vec3 color = mix(col_base, col_grain, grain);
|
||||
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
`
|
||||
},
|
||||
stair: {
|
||||
vertex: `
|
||||
varying vec2 vUv;
|
||||
varying vec3 vNormal;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
vNormal = normalize(normalMatrix * normal);
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying vec2 vUv;
|
||||
varying vec3 vNormal;
|
||||
|
||||
float hash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Acero gris oscuro con textura rayada en los peldaños
|
||||
vec3 base = vec3(0.22, 0.22, 0.23);
|
||||
vec3 edge = vec3(0.35, 0.35, 0.36);
|
||||
|
||||
float stripes = sin(vUv.y * 120.0) * 0.05;
|
||||
float noise = hash(floor(vUv * 60.0)) * 0.06;
|
||||
|
||||
vec3 col = mix(base, edge, abs(vNormal.y) * 0.25 + stripes + noise);
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
`
|
||||
},
|
||||
elevator_cabin: {
|
||||
vertex: `
|
||||
varying vec2 vUv;
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vWorldPosition;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
vNormal = normalize(normalMatrix * normal);
|
||||
vec4 worldPos = modelMatrix * vec4(position, 1.0);
|
||||
vWorldPosition = worldPos.xyz;
|
||||
gl_Position = projectionMatrix * viewMatrix * worldPos;
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying vec2 vUv;
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vWorldPosition;
|
||||
|
||||
float hash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Acero cepillado con reflejos anisotrópicos simulados
|
||||
vec3 metal_dark = vec3(0.28, 0.28, 0.29);
|
||||
vec3 metal_light = vec3(0.48, 0.47, 0.46);
|
||||
|
||||
float grain = hash(floor(vUv * vec2(40.0, 400.0))) * 0.08;
|
||||
float specular = pow(abs(dot(vNormal, vec3(0.0, 1.0, 0.0))), 2.0) * 0.15;
|
||||
|
||||
vec3 col = mix(metal_dark, metal_light, grain + specular);
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
`
|
||||
},
|
||||
elevator_light: {
|
||||
vertex: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragment: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
// Luz blanca cálida fluorescente de tubo típico de oficina
|
||||
vec3 light_color = vec3(0.96, 0.94, 0.85);
|
||||
gl_FragColor = vec4(light_color * 1.4, 1.0);
|
||||
}
|
||||
`
|
||||
},
|
||||
}
|
||||
}
|
||||
166
Public/ecma/Utils/Common.ecma.js
Normal file
166
Public/ecma/Utils/Common.ecma.js
Normal file
@ -0,0 +1,166 @@
|
||||
"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];
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!string} string
|
||||
* @returns {number}
|
||||
*/
|
||||
Common.string_to_integer = string => string.split("").reduce((total, character, j) => (total + character.charCodeAt(0) * (j + 1)) | 0, 0);
|
||||
|
||||
/**
|
||||
* @param {?(number|string)} [seed = null]
|
||||
* @param {!number} [i = 0]
|
||||
* @returns {number}
|
||||
*/
|
||||
Common.random = (seed = null, i = 0) => {
|
||||
if(seed === null)
|
||||
return Math.random() * 0xFFFFFFFF | 0;
|
||||
|
||||
let state, temp;
|
||||
|
||||
if(typeof seed == "string")
|
||||
seed = Common.string_to_integer(seed);
|
||||
|
||||
return (((
|
||||
temp = (
|
||||
temp = Math.imul((
|
||||
state = (seed + Math.imul(i, 0x9E3779B9) + 0x6D2B79F5) & 0xFFFFFFFF
|
||||
) ^ state >>> 15, 1 | state)
|
||||
) + Math.imul(temp ^ temp >>> 7, 61 | temp) ^ temp
|
||||
) ^ temp >>> 14) >>> 0) / 4294967296;
|
||||
};
|
||||
|
||||
Common.random_decay = (value, from, to, decay) => {
|
||||
|
||||
const minimum = Math.min(from, to) | 0,
|
||||
maximum = Math.max(from, to) | 0,
|
||||
// Factor normalizado en escala logarítmica
|
||||
k = Math.max(0.01, Math.min(0.99, decay));
|
||||
|
||||
return Math.min(maximum, minimum + Math.max(0, Math.floor(Math.log(1 - Math.max(0, Math.min(0.999999, value)) * (1 - Math.pow(k, maximum - minimum + 1))) / Math.log(k))));
|
||||
};
|
||||
|
||||
return Common;
|
||||
})();
|
||||
17
Public/ecma/Utils/I18N.ecma.js
Normal file
17
Public/ecma/Utils/I18N.ecma.js
Normal file
@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
|
||||
/** @type {Dictionary.<string, Dictionary.<string, string>>} */
|
||||
const i18n = {
|
||||
espanol : {
|
||||
hollow_bond : "Hollow Bond",
|
||||
menu_title : "HOLLOW BOND",
|
||||
menu_subtitle : "No estás solo en el umbral",
|
||||
menu_new_game : "Nueva partida",
|
||||
menu_options : "Opciones",
|
||||
menu_credits : "Créditos",
|
||||
credits_title : "Créditos",
|
||||
credits_author : "Desarrollo y Diseño: KyMAN",
|
||||
credits_ai : "Co-desarrollo e IA: Gemini",
|
||||
btn_back : "Volver"
|
||||
}
|
||||
};
|
||||
75
Public/ecma/Utils/Settings.ecma.js
Normal file
75
Public/ecma/Utils/Settings.ecma.js
Normal file
@ -0,0 +1,75 @@
|
||||
"use strict";
|
||||
|
||||
/** @type {Dictionary.<string, any|null>} */
|
||||
const settings = {
|
||||
cells : 40,
|
||||
position : "body",
|
||||
class_name : "hollow-bond",
|
||||
frames_per_second : 60,
|
||||
language : "espanol",
|
||||
|
||||
menu_view_class_name : "menu-view",
|
||||
|
||||
// Configuración global de Habitaciones (RoomModel)
|
||||
room_width : 2,
|
||||
room_height : 3,
|
||||
room_depth : 2,
|
||||
room_padding : 0.1,
|
||||
|
||||
// Configuración de Marcos / Conexiones horizontales (FrameModel)
|
||||
frame_width : 1.5,
|
||||
frame_height : 2,
|
||||
frame_depth : 0.1,
|
||||
|
||||
// Configuración de Huecos verticales (HoleModel)
|
||||
hole_width : 4,
|
||||
hole_depth : 4,
|
||||
|
||||
// Configuración de Escaleras (StairsModel)
|
||||
stairs_run : 4, // Longitud del tramo
|
||||
stairs_tread : 1, // Anchura de paso / huella
|
||||
|
||||
// Configuración de Ascensores (ElevatorModel)
|
||||
elevator_width : 2,
|
||||
elevator_height : 2,
|
||||
elevator_depth : 2,
|
||||
elevator_padding : 0.1,
|
||||
|
||||
// Configuración de Entidades / Jugador (EntityModel)
|
||||
entity_width : 0.6,
|
||||
entity_height : 1.8,
|
||||
entity_depth : 0.6,
|
||||
entity_speed : 6.0,
|
||||
|
||||
// Tamaño del cubo del Chunk (50x50x50)
|
||||
chunk_size : 50,
|
||||
floor_height : 3,
|
||||
|
||||
// Canales de Hash para generación procedural (HashManager)
|
||||
hash_channel_chunk : 101,
|
||||
hash_channel_seam_horizontal : 102,
|
||||
hash_channel_seam_vertical_x : 103,
|
||||
hash_channel_seam_vertical_z : 104,
|
||||
hash_channel_elevator : 201,
|
||||
hash_channel_hole : 202,
|
||||
hash_channel_room : 301,
|
||||
hash_channel_frame : 401,
|
||||
hash_channel_stairs : 501,
|
||||
|
||||
// Constantes coprimas de mezcla ortoédrica
|
||||
hash_prime_xf : 0x1B873593,
|
||||
hash_prime_yf : 0x85EBCA6B,
|
||||
hash_prime_zf : 0xC2B2AE35,
|
||||
hash_prime_xt : 0x3C6EF372,
|
||||
hash_prime_yt : 0xBB67AE85,
|
||||
hash_prime_zt : 0xEECE660D,
|
||||
hash_prime_channel : 0x9E3779B9,
|
||||
|
||||
// Fronteras y Marcos
|
||||
seam_frame_probability : 0.35, // Probabilidad de que exista puerta en una sección
|
||||
seam_frame_step : 10, // Distancia entre posibles marcos a lo largo de la pared
|
||||
chunk_frame_probability : 0.15, // Probabilidad de que exista marco en un Chunk
|
||||
chunk_frame_step : 4, // Distancia entre posibles marcos a lo largo de un Chunk
|
||||
|
||||
...(typeof custom_settings == "undefined" ? {} : custom_settings)
|
||||
};
|
||||
345
Public/ecma/Utils/Styles.ecma.js
Normal file
345
Public/ecma/Utils/Styles.ecma.js
Normal file
@ -0,0 +1,345 @@
|
||||
"use strict";
|
||||
|
||||
/** @type {Dictionary.<string, any|null>} */
|
||||
const SCSS = {
|
||||
...{
|
||||
"body,html" : {
|
||||
height : "100%",
|
||||
margin : "0em",
|
||||
background_color : "#050505",
|
||||
overflow : "hidden",
|
||||
user_select : "none"
|
||||
}
|
||||
},
|
||||
...{
|
||||
".hollow-bond" : {
|
||||
position : "relative",
|
||||
top : "0em",
|
||||
left : "0em",
|
||||
width : "100%",
|
||||
height : "100%",
|
||||
font_family : "monospace",
|
||||
color : "#e0e0e0"
|
||||
}
|
||||
},
|
||||
...{
|
||||
".menu-view" : {
|
||||
position : "absolute",
|
||||
top : "0em",
|
||||
left : "0em",
|
||||
width : "100%",
|
||||
height : "100%",
|
||||
display : "flex",
|
||||
flex_direction : "column",
|
||||
align_items : "center",
|
||||
justify_content : "center",
|
||||
background_color : "rgba(8, 8, 10, 0.92)",
|
||||
z_index : "10"
|
||||
},
|
||||
".menu-header" : {
|
||||
text_align : "center",
|
||||
margin_bottom : "3em"
|
||||
},
|
||||
".menu-title" : {
|
||||
font_size : "2.6em",
|
||||
font_weight : "bold",
|
||||
letter_spacing : "0.3em",
|
||||
color : "#d6d0ba",
|
||||
text_shadow : "0 0 0.4em rgba(214, 208, 186, 0.3)"
|
||||
},
|
||||
".menu-subtitle" : {
|
||||
font_size : "0.8em",
|
||||
letter_spacing : "0.15em",
|
||||
color : "#6e6c62",
|
||||
margin_top : "0.6em"
|
||||
},
|
||||
".menu-actions" : {
|
||||
display : "flex",
|
||||
flex_direction : "column",
|
||||
gap : "1em",
|
||||
width : "14em"
|
||||
},
|
||||
".menu-button" : {
|
||||
display : "block",
|
||||
width : "100%",
|
||||
box_sizing : "border-box",
|
||||
padding : "0.7em 1.2em",
|
||||
font_family : "monospace",
|
||||
font_size : "0.9em",
|
||||
color : "#b0aba0",
|
||||
background_color : "rgba(25, 25, 28, 0.7)",
|
||||
border : "0.05em solid #383733",
|
||||
text_align : "center",
|
||||
cursor : "pointer",
|
||||
letter_spacing : "0.1em",
|
||||
transition : "all 0.2s ease",
|
||||
"&:hover" : {
|
||||
color : "#ffffff",
|
||||
background_color : "rgba(45, 45, 52, 0.9)",
|
||||
border_color : "#8c8777"
|
||||
}
|
||||
},
|
||||
".credits-modal" : {
|
||||
position : "absolute",
|
||||
top : "0em",
|
||||
left : "0em",
|
||||
width : "100%",
|
||||
height : "100%",
|
||||
background_color : "rgba(0, 0, 0, 0.85)",
|
||||
display : "flex",
|
||||
flex_direction : "column",
|
||||
align_items : "center",
|
||||
justify_content : "center",
|
||||
gap : "1.2em",
|
||||
z_index : "20"
|
||||
},
|
||||
".credits-box" : {
|
||||
padding : "2em",
|
||||
border : "0.05em solid #42403b",
|
||||
background_color : "#0f0f12",
|
||||
text_align : "center",
|
||||
min_width : "16em",
|
||||
h2 : {
|
||||
margin : "0 0 1em 0",
|
||||
font_size : "1.4em",
|
||||
color : "#d6d0ba",
|
||||
letter_spacing : "0.15em"
|
||||
},
|
||||
p : {
|
||||
margin : "0.6em 0",
|
||||
font_size : "0.85em",
|
||||
color : "#9c988d"
|
||||
}
|
||||
},
|
||||
".new-game-modal" : {
|
||||
position : "absolute",
|
||||
top : "0em",
|
||||
left : "0em",
|
||||
width : "100%",
|
||||
height : "100%",
|
||||
background_color : "rgba(0, 0, 0, 0.85)",
|
||||
display : "flex",
|
||||
flex_direction : "column",
|
||||
align_items : "center",
|
||||
justify_content : "center",
|
||||
z_index : "20"
|
||||
},
|
||||
".new-game-box" : {
|
||||
box_sizing : "border-box",
|
||||
padding : "2.2em 2.5em",
|
||||
border : "0.05em solid #42403b",
|
||||
background_color : "#0f0f12",
|
||||
width : "22em",
|
||||
display : "flex",
|
||||
flex_direction : "column",
|
||||
gap : "1.5em",
|
||||
h2 : {
|
||||
margin : "0 0 0.5em 0",
|
||||
font_size : "1.2em",
|
||||
color : "#d6d0ba",
|
||||
letter_spacing : "0.15em",
|
||||
text_align : "center"
|
||||
}
|
||||
},
|
||||
".field-group" : {
|
||||
display : "flex",
|
||||
flex_direction : "column",
|
||||
gap : "0.6em",
|
||||
label : {
|
||||
font_size : "0.85em",
|
||||
color : "#a8a499",
|
||||
letter_spacing : "0.05em"
|
||||
},
|
||||
".height-value" : {
|
||||
color : "#ffffff",
|
||||
font_weight : "bold"
|
||||
}
|
||||
},
|
||||
".range-input" : {
|
||||
width : "100%",
|
||||
box_sizing : "border-box",
|
||||
accent_color : "#8c8777",
|
||||
cursor : "pointer",
|
||||
background : "transparent"
|
||||
},
|
||||
".text-input,.number-input" : {
|
||||
box_sizing : "border-box",
|
||||
width : "100%",
|
||||
padding : "0.6em 0.8em",
|
||||
background_color : "#17171c",
|
||||
border : "0.05em solid #383733",
|
||||
color : "#ffffff",
|
||||
font_family : "monospace",
|
||||
font_size : "0.9em",
|
||||
letter_spacing : "0.05em",
|
||||
outline : "none",
|
||||
"&:focus" : {
|
||||
border_color : "#8c8777",
|
||||
background_color : "#1c1c22"
|
||||
}
|
||||
},
|
||||
".modal-actions" : {
|
||||
display : "flex",
|
||||
flex_direction : "column",
|
||||
gap : "0.8em",
|
||||
margin_top : "0.8em"
|
||||
},
|
||||
".options-modal" : {
|
||||
position : "absolute",
|
||||
top : "0em",
|
||||
left : "0em",
|
||||
width : "100%",
|
||||
height : "100%",
|
||||
background_color : "rgba(0, 0, 0, 0.85)",
|
||||
display : "flex",
|
||||
flex_direction : "column",
|
||||
align_items : "center",
|
||||
justify_content : "center",
|
||||
z_index : "20"
|
||||
},
|
||||
".options-box" : {
|
||||
box_sizing : "border-box",
|
||||
padding : "2.2em 2.5em",
|
||||
border : "0.05em solid #42403b",
|
||||
background_color : "#0f0f12",
|
||||
width : "22em",
|
||||
display : "flex",
|
||||
flex_direction : "column",
|
||||
gap : "1.5em",
|
||||
h2 : {
|
||||
margin : "0 0 0.5em 0",
|
||||
font_size : "1.2em",
|
||||
color : "#d6d0ba",
|
||||
letter_spacing : "0.15em",
|
||||
text_align : "center"
|
||||
}
|
||||
},
|
||||
".field-group-checkbox" : {
|
||||
display : "flex",
|
||||
align_items : "center",
|
||||
label : {
|
||||
display : "flex",
|
||||
align_items : "center",
|
||||
gap : "0.6em",
|
||||
font_size : "0.85em",
|
||||
color : "#a8a499",
|
||||
cursor : "pointer"
|
||||
}
|
||||
},
|
||||
".checkbox-input" : {
|
||||
accent_color : "#8c8777",
|
||||
cursor : "pointer",
|
||||
width : "1.1em",
|
||||
height : "1.1em"
|
||||
},
|
||||
".fps-hud" : {
|
||||
position : "absolute",
|
||||
top : "0.8em",
|
||||
right : "1em",
|
||||
padding : "0.4em 0.8em",
|
||||
background_color : "rgba(15, 15, 18, 0.8)",
|
||||
border : "0.05em solid #383733",
|
||||
color : "#8c8777",
|
||||
font_size : "0.85em",
|
||||
letter_spacing : "0.1em",
|
||||
z_index : "5",
|
||||
pointer_events : "none",
|
||||
".fps-value" : {
|
||||
color : "#ffffff",
|
||||
font_weight : "bold"
|
||||
}
|
||||
},
|
||||
".hb-canvas-view" : {
|
||||
display : "block",
|
||||
width : "100%",
|
||||
height : "100%",
|
||||
outline : "none",
|
||||
cursor : "crosshair"
|
||||
},
|
||||
|
||||
".select-input" : {
|
||||
"width" : "100%",
|
||||
"box-sizing" : "border-box",
|
||||
"background-color" : "#13141a",
|
||||
"color" : "#e0ded3",
|
||||
"border" : "1px solid #3d4450",
|
||||
"border-radius" : "4px",
|
||||
"padding" : "8px 32px 8px 12px",
|
||||
"font-family" : "inherit",
|
||||
"font-size" : "0.95rem",
|
||||
"outline" : "none",
|
||||
"cursor" : "pointer",
|
||||
"transition" : "border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease",
|
||||
|
||||
/* Quitar flecha nativa del navegador */
|
||||
"appearance" : "none",
|
||||
"-webkit-appearance" : "none",
|
||||
"-moz-appearance" : "none",
|
||||
|
||||
/* Flecha personalizada en SVG limpia y sutil */
|
||||
"background-image" : "url(\"data:image/svg+xml;utf8,<svg fill='%238c92a4' height='20' viewBox='0 0 24 24' width='20' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/></svg>\")",
|
||||
"background-repeat" : "no-repeat",
|
||||
"background-position" : "right 8px center",
|
||||
"background-size" : "18px",
|
||||
|
||||
"&:hover" : {
|
||||
"border-color" : "#606b7d",
|
||||
"background-color" : "#181a22"
|
||||
},
|
||||
"&:focus" : {
|
||||
"border-color" : "#8ca0ba",
|
||||
"box-shadow" : "0 0 0 2px rgba(140, 160, 186, 0.25)"
|
||||
},
|
||||
/* Opciones dentro del desplegable */
|
||||
option : {
|
||||
"background-color" : "#13141a",
|
||||
"color" : "#e0ded3",
|
||||
"padding" : "8px"
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
".inspector-hud" : {
|
||||
"position" : "fixed",
|
||||
"top" : "20px",
|
||||
"left" : "20px",
|
||||
"background-color" : "rgba(19, 20, 26, 0.85)",
|
||||
"backdrop-filter" : "blur(6px)",
|
||||
"border" : "1px solid #3d4450",
|
||||
"border-radius" : "6px",
|
||||
"padding" : "12px 16px",
|
||||
"color" : "#e0ded3",
|
||||
"font-family" : "monospace",
|
||||
"font-size" : "0.85rem",
|
||||
"pointer-events" : "none",
|
||||
"z-index" : "100",
|
||||
"display" : "flex",
|
||||
"flex-direction" : "column",
|
||||
"gap" : "6px",
|
||||
|
||||
".hud-title" : {
|
||||
"color" : "#00bcd4",
|
||||
"font-weight" : "bold",
|
||||
"letter-spacing" : "1px",
|
||||
"margin-bottom" : "4px"
|
||||
},
|
||||
".hud-row" : {
|
||||
"display" : "flex",
|
||||
"justify-content" : "space-between",
|
||||
"gap" : "12px",
|
||||
"b" : {
|
||||
"color" : "#ffeb3b"
|
||||
}
|
||||
},
|
||||
".hud-controls-hint" : {
|
||||
"color" : "#8c92a4",
|
||||
"font-size" : "0.75rem",
|
||||
"margin-top" : "6px",
|
||||
"border-top" : "1px solid #2d333f",
|
||||
"padding-top" : "6px"
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
...(typeof custom_styles == "undefined" ? {} : custom_styles)
|
||||
};
|
||||
42
Public/ecma/Views/CreditsView.ecma.js
Normal file
42
Public/ecma/Views/CreditsView.ecma.js
Normal file
@ -0,0 +1,42 @@
|
||||
"use strict";
|
||||
|
||||
const CreditsView = (function(){
|
||||
|
||||
const CreditsView = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
let root_element = null;
|
||||
|
||||
this.destroy = () => {
|
||||
if(root_element && root_element.parentNode){
|
||||
root_element.remove();
|
||||
root_element = null;
|
||||
};
|
||||
};
|
||||
|
||||
this.go = () => {
|
||||
self.destroy();
|
||||
|
||||
root_element = hollow_bond.html(hollow_bond.item_self, ["div", {"class" : "credits-modal"}, [
|
||||
["div", {"class" : "credits-box"}, [
|
||||
["h2", {"data-i18n" : "credits_title"}, hollow_bond.i18n.get("credits_title", null, "Créditos")],
|
||||
["p", {"data-i18n" : "credits_author"}, hollow_bond.i18n.get("credits_author", null, "Desarrollador: Tú")],
|
||||
["p", {"data-i18n" : "credits_ai"}, hollow_bond.i18n.get("credits_ai", null, "Asistencia Técnica: Gemini")],
|
||||
["button", {
|
||||
"class" : "menu-button",
|
||||
"data-i18n" : "btn_back",
|
||||
"onclick" : () => {
|
||||
self.destroy();
|
||||
hollow_bond.views.menu.go();
|
||||
}
|
||||
}, hollow_bond.i18n.get("btn_back", null, "Atrás")]
|
||||
]]
|
||||
]])[0];
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return CreditsView;
|
||||
})();
|
||||
174
Public/ecma/Views/DungeonHudView.ecma.js
Normal file
174
Public/ecma/Views/DungeonHudView.ecma.js
Normal file
@ -0,0 +1,174 @@
|
||||
"use strict";
|
||||
|
||||
const DungeonHudView = (function(){
|
||||
|
||||
const DungeonHudView = function(hollow_bond, dungeon){
|
||||
|
||||
const self = this;
|
||||
let hud_container = null;
|
||||
|
||||
// Referencias a las etiquetas de texto del HUD
|
||||
const dom = {
|
||||
pos: null,
|
||||
feet: null,
|
||||
chunk: null,
|
||||
room_id: null,
|
||||
room_bounds_xz: null,
|
||||
room_bounds_y: null,
|
||||
room_status: null,
|
||||
physics: null
|
||||
};
|
||||
|
||||
const setup_view = () => {
|
||||
|
||||
// Estilos incrustados para no depender de CSS externo
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
.dungeon-debug-hud {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
background: rgba(10, 12, 16, 0.82);
|
||||
border: 1px solid rgba(0, 188, 212, 0.35);
|
||||
border-radius: 4px;
|
||||
padding: 10px 14px;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
color: #d1d5db;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
line-height: 1.45;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.6);
|
||||
min-width: 290px;
|
||||
}
|
||||
.ddh-title {
|
||||
font-weight: bold;
|
||||
color: #00e5ff;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 6px;
|
||||
border-bottom: 1px solid rgba(0, 188, 212, 0.2);
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
.ddh-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.ddh-label {
|
||||
color: #8892b0;
|
||||
}
|
||||
.ddh-val {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
.ddh-alert {
|
||||
color: #ff5252 !important;
|
||||
}
|
||||
.ddh-ok {
|
||||
color: #69f0ae !important;
|
||||
}
|
||||
.ddh-hint {
|
||||
margin-top: 8px;
|
||||
padding-top: 5px;
|
||||
border-top: 1px dashed rgba(255, 255, 255, 0.15);
|
||||
color: #ffb74d;
|
||||
font-size: 10px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
hud_container = document.createElement("div");
|
||||
hud_container.className = "dungeon-debug-hud";
|
||||
hud_container.innerHTML = `
|
||||
<div class="ddh-title">TELEMETRÍA DEL JUGADOR</div>
|
||||
<div class="ddh-row"><span class="ddh-label">Cámara (X, Y, Z):</span><span class="ddh-val" id="ddh-pos">-</span></div>
|
||||
<div class="ddh-row"><span class="ddh-label">Pies Y (Piso Est.):</span><span class="ddh-val" id="ddh-feet">-</span></div>
|
||||
<div class="ddh-row"><span class="ddh-label">Chunk activo:</span><span class="ddh-val" id="ddh-chunk">-</span></div>
|
||||
<div class="ddh-title" style="margin-top: 6px; color: #ffd54f;">SALA ACTUAL</div>
|
||||
<div class="ddh-row"><span class="ddh-label">ID Sala:</span><span class="ddh-val" id="ddh-room-id">-</span></div>
|
||||
<div class="ddh-row"><span class="ddh-label">Cota XZ Sala:</span><span class="ddh-val" id="ddh-bounds-xz">-</span></div>
|
||||
<div class="ddh-row"><span class="ddh-label">Cota Y Sala (Piso):</span><span class="ddh-val" id="ddh-bounds-y">-</span></div>
|
||||
<div class="ddh-row"><span class="ddh-label">Dentro del Collider:</span><span class="ddh-val" id="ddh-room-status">-</span></div>
|
||||
<div class="ddh-title" style="margin-top: 6px; color: #b388ff;">FÍSICAS / ESTADO</div>
|
||||
<div class="ddh-row"><span class="ddh-label">Suelo firme / Vel Y:</span><span class="ddh-val" id="ddh-physics">-</span></div>
|
||||
<div class="ddh-hint">[WASD] Moverse | [Espacio] Saltar | [Shift] Correr | [Ratón] Mirar</div>
|
||||
`;
|
||||
|
||||
hollow_bond.item_self.appendChild(hud_container);
|
||||
|
||||
dom.pos = hud_container.querySelector("#ddh-pos");
|
||||
dom.feet = hud_container.querySelector("#ddh-feet");
|
||||
dom.chunk = hud_container.querySelector("#ddh-chunk");
|
||||
dom.room_id = hud_container.querySelector("#ddh-room-id");
|
||||
dom.room_bounds_xz = hud_container.querySelector("#ddh-bounds-xz");
|
||||
dom.room_bounds_y = hud_container.querySelector("#ddh-bounds-y");
|
||||
dom.room_status = hud_container.querySelector("#ddh-room-status");
|
||||
dom.physics = hud_container.querySelector("#ddh-physics");
|
||||
};
|
||||
|
||||
this.tick = () => {
|
||||
if(!dungeon || !dungeon.camera || !hud_container) return;
|
||||
|
||||
const pos = dungeon.camera.position;
|
||||
const floor_h = hollow_bond.room_height || 3;
|
||||
const controls = hollow_bond.controls?.game;
|
||||
const eye_height = controls?.current_height || 1.75;
|
||||
const feet_y = pos.y - eye_height;
|
||||
const estimated_floor = Math.round(feet_y / floor_h);
|
||||
|
||||
// 1. Posición y piso estimado
|
||||
dom.pos.textContent = `${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)}`;
|
||||
dom.feet.textContent = `${feet_y.toFixed(2)}m (Piso: ${estimated_floor})`;
|
||||
|
||||
// 2. Coordenadas de Chunk
|
||||
const c = dungeon.current_chunk;
|
||||
dom.chunk.textContent = `[${c.x}, ${c.y}, ${c.z}]`;
|
||||
|
||||
// 3. Sala activa
|
||||
const cur_room = dungeon.current_room;
|
||||
|
||||
if(cur_room && cur_room.model){
|
||||
const m = cur_room.model;
|
||||
const room_py = Math.round(m.y / m.height);
|
||||
const is_inside = cur_room.collider ? cur_room.collider.contains(pos.x, feet_y, pos.z) : false;
|
||||
|
||||
dom.room_id.textContent = m.id;
|
||||
dom.room_bounds_xz.textContent = `X:[${m.x}..${(m.x + m.width)}] Z:[${m.z}..${(m.z + m.depth)}]`;
|
||||
dom.room_bounds_y.textContent = `Y:[${m.y}..${(m.y + m.height)}] (Piso: ${room_py})`;
|
||||
|
||||
if(is_inside){
|
||||
dom.room_status.textContent = "SÍ (OK)";
|
||||
dom.room_status.className = "ddh-val ddh-ok";
|
||||
} else {
|
||||
dom.room_status.textContent = "⚠️ FUERA DE LÍMITES";
|
||||
dom.room_status.className = "ddh-val ddh-alert";
|
||||
}
|
||||
} else {
|
||||
dom.room_id.textContent = "SIN ASIGNAR";
|
||||
dom.room_bounds_xz.textContent = "-";
|
||||
dom.room_bounds_y.textContent = "-";
|
||||
dom.room_status.textContent = "⚠️ EN EL VACÍO";
|
||||
dom.room_status.className = "ddh-val ddh-alert";
|
||||
}
|
||||
|
||||
// 4. Estado de físicas
|
||||
if(controls){
|
||||
const grounded_str = controls.is_grounded ? "Suelo: SÍ" : "Suelo: NO";
|
||||
const vel_str = `VelY: ${(controls.velocity_y || 0).toFixed(2)}`;
|
||||
dom.physics.textContent = `${grounded_str} | ${vel_str}`;
|
||||
dom.physics.className = controls.is_grounded ? "ddh-val ddh-ok" : "ddh-val";
|
||||
}
|
||||
};
|
||||
|
||||
this.destroy = () => {
|
||||
if(hud_container && hud_container.parentNode){
|
||||
hud_container.parentNode.removeChild(hud_container);
|
||||
hud_container = null;
|
||||
}
|
||||
};
|
||||
|
||||
setup_view();
|
||||
};
|
||||
|
||||
return DungeonHudView;
|
||||
})();
|
||||
59
Public/ecma/Views/FpsView.ecma.js
Normal file
59
Public/ecma/Views/FpsView.ecma.js
Normal file
@ -0,0 +1,59 @@
|
||||
"use strict";
|
||||
|
||||
const FpsView = (function(){
|
||||
|
||||
const FpsView = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
let root_element = null,
|
||||
frame_count = 0,
|
||||
last_time = performance.now();
|
||||
|
||||
this.destroy = () => {
|
||||
if(root_element && root_element.parentNode){
|
||||
root_element.remove();
|
||||
root_element = null;
|
||||
};
|
||||
};
|
||||
|
||||
this.go = () => {
|
||||
self.destroy();
|
||||
|
||||
root_element = hollow_bond.html(hollow_bond.item_self, ["div", {
|
||||
"class" : "fps-hud"
|
||||
}, [
|
||||
["span", {"class" : "fps-label"}, "FPS: "],
|
||||
["span", {"class" : "fps-value"}, "60"]
|
||||
]])[0];
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
/**
|
||||
* Llamado desde el bucle de render principal (requestAnimationFrame)
|
||||
*/
|
||||
this.tick = () => {
|
||||
if(!root_element)
|
||||
return;
|
||||
|
||||
const now = performance.now();
|
||||
|
||||
frame_count++;
|
||||
|
||||
if(now - last_time >= 1000){
|
||||
|
||||
const value = root_element.querySelector(".fps-value");
|
||||
|
||||
if(value)
|
||||
value.textContent = String(frame_count);
|
||||
|
||||
frame_count = 0;
|
||||
last_time = now;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return FpsView;
|
||||
})();
|
||||
73
Public/ecma/Views/MenuView.ecma.js
Normal file
73
Public/ecma/Views/MenuView.ecma.js
Normal file
@ -0,0 +1,73 @@
|
||||
"use strict";
|
||||
|
||||
const MenuView = (function(){
|
||||
|
||||
const MenuView = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
let root_element = null;
|
||||
|
||||
this.destroy = () => {
|
||||
if(root_element && root_element.parentNode){
|
||||
root_element.remove();
|
||||
root_element = null;
|
||||
};
|
||||
};
|
||||
|
||||
const on_click_new_game = () => {
|
||||
self.destroy();
|
||||
hollow_bond.views.new_game.go();
|
||||
};
|
||||
|
||||
const on_click_options = () => {
|
||||
self.destroy();
|
||||
hollow_bond.views.options && hollow_bond.views.options.go();
|
||||
};
|
||||
|
||||
const on_click_credits = () => {
|
||||
self.destroy();
|
||||
hollow_bond.views.credits.go();
|
||||
};
|
||||
|
||||
this.go = () => {
|
||||
self.destroy();
|
||||
|
||||
root_element = hollow_bond.html(hollow_bond.item_self, ["div", {
|
||||
"class" : hollow_bond.settings.get("menu_view_class_name", null, "menu-view")
|
||||
}, [
|
||||
["div", {"class" : "menu-header"}, [
|
||||
["div", {
|
||||
"class" : "menu-title",
|
||||
"data-i18n" : "menu_title"
|
||||
}, hollow_bond.i18n.get("menu_title", null, "HOLLOWBOND")],
|
||||
["div", {
|
||||
"class" : "menu-subtitle",
|
||||
"data-i18n" : "menu_subtitle"
|
||||
}, hollow_bond.i18n.get("menu_subtitle", null, "PROCEDURAL DUNGEON ENGINE")]
|
||||
]],
|
||||
["div", {"class" : "menu-actions"}, [
|
||||
["button", {
|
||||
"class" : "menu-button",
|
||||
"data-i18n" : "menu_new_game",
|
||||
"onclick" : on_click_new_game
|
||||
}, hollow_bond.i18n.get("menu_new_game", null, "Nueva Partida")],
|
||||
["button", {
|
||||
"class" : "menu-button",
|
||||
"data-i18n" : "menu_options",
|
||||
"onclick" : on_click_options
|
||||
}, hollow_bond.i18n.get("menu_options", null, "Opciones")],
|
||||
["button", {
|
||||
"class" : "menu-button",
|
||||
"data-i18n" : "menu_credits",
|
||||
"onclick" : on_click_credits
|
||||
}, hollow_bond.i18n.get("menu_credits", null, "Créditos")]
|
||||
]]
|
||||
]])[0];
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return MenuView;
|
||||
})();
|
||||
152
Public/ecma/Views/NewGameView.ecma.js
Normal file
152
Public/ecma/Views/NewGameView.ecma.js
Normal file
@ -0,0 +1,152 @@
|
||||
"use strict";
|
||||
|
||||
const NewGameView = (function(){
|
||||
|
||||
const NewGameView = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
let root_element = null;
|
||||
|
||||
/**
|
||||
* Normaliza la entrada de semilla: enteros si son puros dígitos,
|
||||
* o hash determinista de 32 bits para cadenas alfanuméricas.
|
||||
*/
|
||||
const parse_seed = raw => {
|
||||
|
||||
const clean = String(raw).trim();
|
||||
|
||||
if(/^\d+$/.test(clean))
|
||||
return parseInt(clean, 10);
|
||||
|
||||
let hash = 0;
|
||||
|
||||
for(let i = 0; i < clean.length; i++)
|
||||
hash = (hash << 5) - hash + clean.charCodeAt(i) | 0;
|
||||
|
||||
return Math.abs(hash);
|
||||
};
|
||||
|
||||
this.destroy = () => {
|
||||
if(root_element && root_element.parentNode){
|
||||
root_element.remove();
|
||||
root_element = null;
|
||||
}
|
||||
};
|
||||
|
||||
this.go = () => {
|
||||
self.destroy();
|
||||
|
||||
// const default_seed = Math.floor(Math.random() * 10000000);
|
||||
// const default_seed = 7229736;
|
||||
// const default_seed = 9003770; // Bug en ascensor y paredes.
|
||||
// const default_seed = 1501172; // Ascensor 1ª planta. Encerrado.
|
||||
const default_seed = 8030220; // Ascensor 1ª planta.
|
||||
|
||||
// 7229736
|
||||
console.log(default_seed);
|
||||
|
||||
root_element = hollow_bond.html(hollow_bond.item_self, ["div", {"class" : "new-game-modal"}, [
|
||||
["div", {"class" : "new-game-box"}, [
|
||||
["h2", {"data-i18n" : "new_game_title"}, hollow_bond.i18n.get("new_game_title", null, "Configuración de Partida")],
|
||||
|
||||
// Selector de Modo de Ejecución
|
||||
["div", {"class" : "field-group"}, [
|
||||
["label", {"data-i18n" : "mode_label"}, hollow_bond.i18n.get("mode_label", null, "Modo de Ejecución:")],
|
||||
["select", {"class" : "select-input input-game-mode"}, [
|
||||
["option", {"value" : "gameplay", "selected" : "selected"}, hollow_bond.i18n.get("mode_gameplay", null, "Partida Normal (Primera Persona)")],
|
||||
["option", {"value" : "inspector"}, hollow_bond.i18n.get("mode_inspector", null, "Inspector de Pruebas (Capas/Pisos)")]
|
||||
]]
|
||||
]],
|
||||
|
||||
// Selector de altura del personaje
|
||||
["div", {"class" : "field-group field-player-height"}, [
|
||||
["label", {}, [
|
||||
["span", {"data-i18n" : "player_height_label"}, hollow_bond.i18n.get("player_height_label", null, "Altura del personaje: ")],
|
||||
["span", {"class" : "height-value"}, "1.75"],
|
||||
["span", {}, " m"]
|
||||
]],
|
||||
["input", {
|
||||
"type" : "range",
|
||||
"min" : "1.30",
|
||||
"max" : "2.35",
|
||||
"step" : "0.01",
|
||||
"value" : "1.75",
|
||||
"class" : "range-input",
|
||||
"oninput" : input => {
|
||||
input.closest(".field-group").querySelector(".height-value").textContent = parseFloat(input.value).toFixed(2);
|
||||
}
|
||||
}]
|
||||
]],
|
||||
|
||||
// Campo de semilla
|
||||
["div", {"class" : "field-group"}, [
|
||||
["label", {"data-i18n" : "seed_label"}, hollow_bond.i18n.get("seed_label", null, "Semilla:")],
|
||||
["input", {
|
||||
"type" : "text",
|
||||
"class" : "text-input",
|
||||
"value" : String(default_seed)
|
||||
}]
|
||||
]],
|
||||
|
||||
// Campo de Chunks máximos (0 = Infinito)
|
||||
["div", {"class" : "field-group"}, [
|
||||
["label", {}, [
|
||||
["span", {"data-i18n" : "max_chunks_label"}, hollow_bond.i18n.get("max_chunks_label", null, "Máximo de Chunks: ")],
|
||||
["span", {"class" : "max-chunks-desc"}, "(0 = Infinito)"]
|
||||
]],
|
||||
["input", {
|
||||
"type" : "number",
|
||||
"min" : "0",
|
||||
"step" : "1",
|
||||
"value" : "0",
|
||||
"class" : "number-input input-max-chunks"
|
||||
}]
|
||||
]],
|
||||
|
||||
// Acciones
|
||||
["div", {"class" : "modal-actions"}, [
|
||||
["button", {
|
||||
"class" : "menu-button",
|
||||
"data-i18n" : "btn_start",
|
||||
"onclick" : button => {
|
||||
const box = button.closest(".new-game-box"),
|
||||
mode = box.querySelector(".input-game-mode").value,
|
||||
height = parseFloat(box.querySelector(".range-input").value) || 1.75,
|
||||
raw_seed = box.querySelector(".text-input").value || default_seed,
|
||||
final_seed = parse_seed(raw_seed),
|
||||
maximum_chunks = parseInt(box.querySelector(".input-max-chunks").value) || 0;
|
||||
|
||||
self.destroy();
|
||||
|
||||
// 1. Generar mapa determinista
|
||||
hollow_bond.map.generate(final_seed, maximum_chunks, () => {
|
||||
// 2. Enrutamiento según el modo seleccionado
|
||||
if(mode === "inspector"){
|
||||
hollow_bond.inspector && hollow_bond.inspector.start();
|
||||
} else {
|
||||
hollow_bond.dungeon && hollow_bond.dungeon.start(height);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}, hollow_bond.i18n.get("btn_start", null, "Iniciar")],
|
||||
|
||||
["button", {
|
||||
"class" : "menu-button",
|
||||
"data-i18n" : "btn_back",
|
||||
"onclick" : () => {
|
||||
self.destroy();
|
||||
hollow_bond.views.menu.go();
|
||||
}
|
||||
}, hollow_bond.i18n.get("btn_back", null, "Atrás")]
|
||||
]]
|
||||
]]
|
||||
]])[0];
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return NewGameView;
|
||||
})();
|
||||
103
Public/ecma/Views/OptionsView.ecma.js
Normal file
103
Public/ecma/Views/OptionsView.ecma.js
Normal file
@ -0,0 +1,103 @@
|
||||
"use strict";
|
||||
|
||||
const OptionsView = (function(){
|
||||
|
||||
const OptionsView = function(hollow_bond){
|
||||
|
||||
const self = this;
|
||||
let root_element = null;
|
||||
|
||||
this.destroy = () => {
|
||||
if(root_element && root_element.parentNode){
|
||||
root_element.remove();
|
||||
root_element = null;
|
||||
};
|
||||
};
|
||||
|
||||
this.go = () => {
|
||||
self.destroy();
|
||||
|
||||
const current_scale = hollow_bond.render_scale,
|
||||
current_fps = hollow_bond.show_fps;
|
||||
|
||||
root_element = hollow_bond.html(hollow_bond.item_self, ["div", {"class" : "options-modal"}, [
|
||||
["div", {"class" : "options-box"}, [
|
||||
["h2", {"data-i18n" : "options_title"}, hollow_bond.i18n.get("options_title", null, "Opciones")],
|
||||
|
||||
// Selector de calidad / Render Scale
|
||||
["div", {"class" : "field-group"}, [
|
||||
["label", {}, [
|
||||
["span", {"data-i18n" : "render_scale_label"}, hollow_bond.i18n.get("render_scale_label", null, "Calidad del Canvas: ")],
|
||||
["span", {"class" : "scale-value"}, String(current_scale)],
|
||||
["span", {}, "%"]
|
||||
]],
|
||||
["input", {
|
||||
"type" : "range",
|
||||
"min" : "3.125",
|
||||
"max" : "100",
|
||||
"step" : "3.125",
|
||||
"value" : String(current_scale),
|
||||
"class" : "range-input",
|
||||
"oninput" : input => {
|
||||
input.closest(".field-group").querySelector(".scale-value").textContent = parseFloat(input.value).toFixed(3).replace(/\.?0+$/, "");
|
||||
}
|
||||
}]
|
||||
]],
|
||||
|
||||
// Checkbox para ver FPS
|
||||
["div", {"class" : "field-group-checkbox"}, [
|
||||
["label", {}, [
|
||||
["input", {
|
||||
"type" : "checkbox",
|
||||
"class" : "checkbox-input",
|
||||
...(current_fps ? {"checked" : "checked"} : {})
|
||||
}],
|
||||
["span", {"data-i18n" : "show_fps_label"}, hollow_bond.i18n.get("show_fps_label", null, "Mostrar indicador de FPS")]
|
||||
]]
|
||||
]],
|
||||
|
||||
// Botonera de acciones
|
||||
["div", {"class" : "modal-actions"}, [
|
||||
["button", {
|
||||
"class" : "menu-button",
|
||||
"data-i18n" : "btn_save",
|
||||
"onclick" : button => {
|
||||
|
||||
const box = button.closest(".options-box"),
|
||||
new_scale = parseFloat(box.querySelector(".range-input").value) || 100,
|
||||
new_fps = box.querySelector(".checkbox-input").checked || false;
|
||||
|
||||
// Guardamos en memoria y en settings
|
||||
hollow_bond.render_scale = new_scale;
|
||||
hollow_bond.show_fps = new_fps;
|
||||
|
||||
// Si DungeonManager ya está corriendo, refrescar resolución del canvas
|
||||
hollow_bond.dungeon &&
|
||||
hollow_bond.dungeon.update_canvas_resolution &&
|
||||
hollow_bond.dungeon.update_canvas_resolution();
|
||||
|
||||
self.destroy();
|
||||
hollow_bond.views.menu.go();
|
||||
|
||||
}
|
||||
}, hollow_bond.i18n.get("btn_save", null, "Guardar")],
|
||||
|
||||
["button", {
|
||||
"class" : "menu-button",
|
||||
"data-i18n" : "btn_back",
|
||||
"onclick" : () => {
|
||||
self.destroy();
|
||||
hollow_bond.views.menu.go();
|
||||
}
|
||||
}, hollow_bond.i18n.get("btn_back", null, "Atrás")]
|
||||
]]
|
||||
]]
|
||||
]])[0];
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return OptionsView;
|
||||
})();
|
||||
118
Public/index.html
Normal file
118
Public/index.html
Normal file
@ -0,0 +1,118 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<title data-i18n="hollow_bond">Hollow Bond</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://hollowbond.k3y.pw/" />
|
||||
<meta name="xdoc:git" content="https://git.k3y.pw/KyMAN/HollowBond" />
|
||||
<meta name="xdoc:authors" content="KyMAN" />
|
||||
<meta name="xdoc:ai-authors" content="Gemini" />
|
||||
<meta name="xdoc:since" content="20260922" />
|
||||
<meta name="xdoc:version" content="20261010" />
|
||||
<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="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js" charset="utf-8"></script>
|
||||
|
||||
<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/Managers/HashManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/MapManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/InspectorManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/DungeonManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/ThemesManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Controls/DebugControls.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Controls/GameControls.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Builders/FramesBuilder.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Builders/StairsBuilder.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Builders/HolesBuilder.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Builders/ElevatorsBuilder.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Builders/RoomsBuilder.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/FrameModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/HoleModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/ElevatorModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/StairsModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/RoomModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Models/ChunkModel.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Themes/Level0Theme.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Colliders/RoomCollider.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Colliders/FrameCollider.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Colliders/RailingCollider.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Colliders/StairCollider.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Colliders/ElevatorCollider.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Geometries/Debug/FrameGeometryDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Geometries/Debug/HoleGeometryDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Geometries/Debug/ElevatorGeometryDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Geometries/Debug/StairGeometryDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Geometries/Debug/RoomGeometryDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Geometries/Game/RoomGeometryGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Geometries/Game/FrameGeometryGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Geometries/Game/HoleGeometryGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Geometries/Game/RailingGeometryGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Geometries/Game/StairGeometryGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Geometries/Game/ElevatorGeometryGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Geometries/Game/ElevatorFrameGeometryGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Materials/Debug/FrameMaterialDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Materials/Debug/HoleMaterialDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Materials/Debug/ElevatorMaterialDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Materials/Debug/StairMaterialDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Materials/Debug/RoomMaterialDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Materials/Game/RoomMaterialGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Materials/Game/FrameMaterialGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Materials/Game/HoleMaterialGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Materials/Game/RailingMaterialGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Materials/Game/StairMaterialGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Materials/Game/ElevatorMaterialGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Objects/Debug/FrameObjectDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Objects/Debug/HoleObjectDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Objects/Debug/ElevatorObjectDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Objects/Debug/StairObjectDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Objects/Debug/RoomObjectDebug.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Objects/Game/RoomObjectGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Objects/Game/FrameObjectGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Objects/Game/HoleObjectGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Objects/Game/RailingObjectGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Objects/Game/StairObjectGame.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Objects/Game/ElevatorObjectGame.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/MenuView.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/NewGameView.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/FpsView.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/DungeonHudView.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||
|
||||
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Application/HollowBond.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 hollow_bond = new HollowBond();
|
||||
|
||||
</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 `hollow_bond.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 `hollow_bond.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 `hollow_bond.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/HollowBond.ecma.js`.
|
||||
|
||||
```js
|
||||
"use strict";
|
||||
|
||||
const HollowBond = (function(){
|
||||
|
||||
const HollowBond = 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, "hollow-bond");
|
||||
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 HollowBond;
|
||||
})();
|
||||
|
||||
```
|
||||
|
||||
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(hollow_bond){
|
||||
|
||||
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(hollow_bond){
|
||||
|
||||
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(hollow_bond.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 : "hollow-bond",
|
||||
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"
|
||||
}
|
||||
},
|
||||
...{
|
||||
".hollow-bond" : {
|
||||
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="hollow_bond">Hollow Bond</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://hollowbond.k3y.pw/" />
|
||||
<meta name="xdoc:git" content="https://git.k3y.pw/KyMAN/HollowBond" />
|
||||
<meta name="xdoc:authors" content="KyMAN" />
|
||||
<meta name="xdoc:ai-authors" content="Gemini" />
|
||||
<meta name="xdoc:since" content="20260922" />
|
||||
<meta name="xdoc:version" content="20260922" />
|
||||
<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/HollowBond.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 hollow_bond = new HollowBond();
|
||||
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
```
|
||||
21
Public/md/chat-gpt.md
Normal file
21
Public/md/chat-gpt.md
Normal file
@ -0,0 +1,21 @@
|
||||
Buenos días, ChatGPT. ¿Qué tal? Voy a hacer un experimento por una cosa que me dijo un amigo, a ver qué sale de esto. La idea es que te voy a solicitar que me hagas un juego para navegador el cual se llamara **HollowBond** el cual ha de funcionar tanto con protocolo `HTTP://` como por protocolo `FILE://`, es decir, JS no puede tener importaciones, sino que han de adjuntarse los ficheros en las etiquetas HTML `script`. Por otro lado, el proyecto quiero que se divida de la siguiente forma: un directorio raíz con el nombre del juego donde encontremos un `README.md` con la documentación básica del proyecto y si hiciera falta, un índice a otros archivos MarkDown los cuales estarán, si hicieren falta, en un directorio llamado `MD`; un archivo `.gitignore` donde se ignore el `/Data`, `/Public/data`, los archivos de caché de SASS y cualquier archivo que contenga las palabra `secret`; luego, un directorio llamado `Tools` donde se meterán Scripts que sean necesarios para cualquier funcionalidad adicional que se requiera; luego tendríamos un directorio llamado `Public` donde estará la parte pública del proyecto el cual tendrá el archivo `index.html`, una carpeta `data` donde se puedan meter archivos diversos no pertenecientes al proyecto o que éstos sean alterables o generables por los usuarios finales, así como Assets externos, como puede ser la librería de ThreeJS que no sean propios del proyecto; una carpeta `ecma` donde se estructurará el proyecto JS; y un directorio `scss` con los archivos de estilos necesarios.
|
||||
|
||||
Los archivos SCSS será uno que una todos, otro que sea la configuración y otro que sean los estilos en cuestión. Si hiciere falta meter más, se meten más, y la compilación se haría ahí mismo. El archivo principal se llamaría `HollowBond.scss` y el archivo compilado sería `HollowBond.css`. En el directorio `Tools` meteremos un archivo `bat` y otro `sh` para poder llamarlos y compilar el SASS.
|
||||
|
||||
Por otro lado, tenemos los archivos JS, los cuales estarán todos dentro de `/Public/ecma` en un árbol de directorios los cuales son: `Application`, donde se almacenarán los archivos propios de aplicación como puede ser el archivo principal `HollowBond.ecma.js` entre otros posibles; por otro lado tenemos el directorio de gestores llamado `Managers` para tener gestores como de configuración (`SettingsManager.ecma.js`), I18N (`I18NManager.ecma.js`) , o cualquier otro que pueda ser necesario; por otro lado tendríamos un directorio para los modelos JS llamado `Models`; por otro lado un directorio `Utils` para almacenar elementos comunes como puede ser el archivo `Common.ecma.js` para los métodos estáticos comunes y reutilizables a todo el proyecto, un archivo `I18N.ecma.js` donde se almacenen los textos internacionalizados en un diccionario anidado en otro donde el primer nivel indica el idioma y el segundo la clave y el texto asociado, y `Settings.ecma.js` para almacenar un diccionario con la configuración; por otro lado, un directorio `Textures` para almacenar las texturas que se usen en el juego; otro directorio para las vistas HTML generadas desde JS llamado `Views`; y por otro lado, posiblemente haga falta crear más directorios de división según se vaya viendo durante el desarrollo del propio juego.
|
||||
|
||||
El juego está inspirado en los **Backrooms** y estará desarrollado inicialmente 100% JS y SCSS. La idea del juego es empezar en una habitación que a su vez está unida a otras habitaciones ya sea por huecos de forma vertical que pueden tener escaleras; marcos para unir de forma horizontal; y ascensores para unir las habitaciones de forma teletransportada. Para dicho fin hay que pensar un sistema de semilla tanto por valor entero como por String cuyo valor salga de la suma del valor de cada caracter desplazado por su posición por byte, de esta forma, un valor puede ser un nombre y siempre dará el mismo mapa.
|
||||
|
||||
El mapa se ha cubrir en dos fases diferenciadas: por un sistema lógico que almacene la creación de los distintos elementos en base a la semilla; y un sistema que vaya dibujando el mapa según avance el jugador por el mismo, es decir, el mapa se genera por semilla en el apartado lógico, y en el apartado visual se van cargando habitaciones hasta un límite de 5 niveles de habitaciones visibles a partir de la habitación donde se encuentre el jugador donde a medida que cambie de habitación se crearán las nuevas necesarias y se eliminarán las no visibles, de esta manera, el mapa se puede hacer infinito. La generación de los elementos ha de ser por unidades enteras salvo ciertas excepciones que se irán viendo. Las habitaciones tendrán 3 unidades de altura y de ancho y largo será variable aleatoriamente entre 2 y 25 donde cuanto más grande, más raro es que salga y tendrá un Padding paras encoge un poco la habitación para dar lugar al ancho de las paredes y del techo y suelo cuando se vayan uniendo habitaciones, inicialmente de 0.1. Cada habitación tendrá probabilidades de que salgan más a su alrededor accesibles desde la misma horizontalmente, unidas entre ellas por un marco que tendrá una profundidad de dos veces el padding de la habitación. Dos habitaciones pueden tener más de un marco de unión pero la probabilidad será baja. Una habitación tendrá una probabilidad de tener un hueco en el suelo, en el techo o en ambos que ocupe mínimo 4 unidades por lado a un máximo del tamaño de la habitación menos 2 por cada lado para dejar un espacio para poder caminar. Un hueco puede unir verticalmente entre 2 y 25 habitaciones, donde a más habitaciones unidas, más raro. Cada habitación unida por un hueco se cargará visualmente, y sólo las contiguas donde se encuentre el jugador se cargará el segundo nivel de habitaciones. Por lo general, las habitaciones unidas por un hueco serán iguales, pero habrá una probabilidad en la cual sean distintas. Un hueco puede tener escaleras que unan dos pisos consecutivos. También habrá una probabilidad de que una habitación tenga un acceso a un ascensor, el cual puede unir entre 3 y 130 habitaciones donde a más habitaciones unidas, menos probabilidad. Si un mapa se queda sin poder avanzar más por ningún lado, se generará un ascensor en una habitación aún no recorrida por el jugador, de esta forma permanecerá siempre la infinidad del mapa. Todos los valores dados en este párrafo estarán en `Settings.ecma.js` para poderlos configurar. Las habitaciones, los marcos, los huecos, las escaleras y los ascensores serán modelos distintos entre ellos en la carpeta `Models`.
|
||||
|
||||
El juego tendrá un sistema de gravedad donde caigan todas las entidades cargadas hasta colisionar con algo concreto. Las entidades serían jugadores y cosas así. Por defecto, la gravedad será 9.81 para que sea como en la realidad terrestre.
|
||||
|
||||
El jugador será una cápsula vertical ante las colisiones, es decir, siempre estará en vertical y con la forma de la cápsula podrá subir y bajar escaleras y sortear ciertos obstáculos que pueda haber en el escenario. Todos los modelos contendrán su sistema de colisiones. Los huecos tendrán barandillas para evitar que el jugador caiga por ellos, pero si el jugador salta lo suficiente, la barandilla podrá saltarse y por tanto caer. Las escaleras serán de rejilla con peldaños unidos por dos guías laterales y empiezan en una base o descansillo y acaban en otra base o descansillo y tendrá en su conjunto 4 unidades de largo, 1 unidad de ancho y 3 unidades más dos veces el padding de las habitaciones y tendrán barandillas que impidan que el jugador caiga por el hueco salvo que el mismo las salte, y tendrá una apertura junto con los huecos que una para poder entrar y salir de ellas. Las barandillas tendrán un alto de 1 unidad. Los marcos y los huecos tendrán una capa para cubrir la unión de las habitaciones. El jugador medirá entre 1.3 y 2.4 unidades de altura, seleccionable en el menú cuando se vaya a iniciar una partida, y saltará la mitad de su altura y se agachará a la mitad de su altura, y su ancho será un cuarto de su altura. Todas estas unidades aquí mostradas estarán configurables en la propia configuración. El jugador irá a 0.5 de la velocidad andando cuando esté agachado y corriendo irá a una velocidad de 2.3 sobre la velocidad andando. También tendrá un botón de acción con lo que llamar al ascensor y luego, para elegir un piso en los botones del mismo ascensor. La vista será en primera persona y tendrá un punto centrado para indicar donde está apuntando el jugador. Todo ello estará en la configuración.
|
||||
|
||||
Los ascensores, por defecto estarán todos cerrados. La idea es que el jugador llegue, lo llame, tarde unos 2 o 3 segundos, suene una campanita o algo, se abran las puertas y el jugador pueda entrar. Los ascensores medirán 2x2 unidades con un padding como el de las habitaciones. En uno de los lados tendrá la botonera para elegir un piso. Una vez seleccionado el piso, se cierren las puertas, se cargue la nueva ubicación con sus respectivas habitaciones y tras 2 o 3 segundos suene la campanita y se abran las puertas. Los pisos serán siempre los mismos y el piso actual se verá indicado en los botones.
|
||||
|
||||
Las habitaciones tendrán unas lámparas fluorescentes que darán luz, la cual será configurable desde la propia configuración. La lámpara de fluorescentes será un modelo dentro de la carpeta `Model` que integrará la habitación para que ésta tenga luz. Para evitar sombras extrañas o disfuncionales, la luz tendrá un radio que exceda de la altura del bloque de la lámpara, así quedaría todo iluminado, y como éstas son largas, hará efecto con ese factor, aunque su radio también será configurable.
|
||||
|
||||
Cuando abres el juego se mostrará un menú donde te permita iniciar una nueva partida, también te permita cargar una partida, otro botón para la configuración y opciones, otro botón para mostrar los créditos. En los créditos simplemente salgamos tú y yo, yo como KyMAN. En las Opciones quiero que salga un deslizable que sea la calidad del CANVAS sobre el que se dibuje el juego desde el ThreeJS donde para lograrlo usaremos el tamaño de CSS y el tamaño por atributos de la etiqueta CANVAS. La barra de desplazamiento tendrá un `step` de 3.125 desde mínimo 3.125 hasta 100 para hacer que la calidad será ese porcentaje; por otro lado, la configuración de los movimientos del usuario donde WASD sea para moverse adelante, atrás, desplazamiento lateral izquierdo y desplazamiento lateral derecho, IJKL para girar hacia arriba, hacia abajo, hacia la izquierda y hacia la derecha, luego, la tecla de saltar, que por defecto será espacio, agacharse será Shift izquierdo y acción C. Por defecto, el ratón es usable y tiene la tecla acción en el botón izquierdo del mismo, para agacharse es el botón derecho y mover hacia arriba, hacia abajo, hacia la izquierda y hacia la derecha con el propio ratón. Habrá una barra para configurar la sensibilidad del ratón. Cuando le damos a una nueva partida nos mandará seleccionar una semilla que por defecto será un valor entero aleatorio, y nos mandará poner la altura que tenemos con una barra deslizable, y tras ello sería empezar en la primera habitación.
|
||||
|
||||
ChatGPT. ¿Ves esto posible? ¿Me puedes hacer un archivo ZIP con todo esto?
|
||||
Loading…
Reference in New Issue
Block a user