fixed bugs
This commit is contained in:
@@ -88,7 +88,7 @@ export const globalSettings: ConfigUiParamsCategories[] = [
|
|||||||
values: [
|
values: [
|
||||||
30,
|
30,
|
||||||
150,
|
150,
|
||||||
30
|
33
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -106,7 +106,7 @@ export const globalSettings: ConfigUiParamsCategories[] = [
|
|||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
15,
|
15,
|
||||||
9
|
8
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-10,
|
-10,
|
||||||
@@ -125,7 +125,7 @@ export const globalSettings: ConfigUiParamsCategories[] = [
|
|||||||
[
|
[
|
||||||
-360,
|
-360,
|
||||||
360,
|
360,
|
||||||
5
|
7
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-360,
|
-360,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { EasyEvent, JoystickC, UpdateController } from "@24tools/playable_templa
|
|||||||
import { Vector3 } from "three";
|
import { Vector3 } from "three";
|
||||||
import { PhysicsTriggerC } from "./PhysicsTriggerC";
|
import { PhysicsTriggerC } from "./PhysicsTriggerC";
|
||||||
import { PropC, PropRegistry } from "./PropC";
|
import { PropC, PropRegistry } from "./PropC";
|
||||||
|
import { PropHpUIC } from "./PropHpUIC";
|
||||||
import { Player } from "../Presets/Player";
|
import { Player } from "../Presets/Player";
|
||||||
|
|
||||||
export class GatherC {
|
export class GatherC {
|
||||||
@@ -119,6 +120,7 @@ export class GatherC {
|
|||||||
if (Player.isMoving() || Player.isCombatBusy()) return;
|
if (Player.isMoving() || Player.isCombatBusy()) return;
|
||||||
|
|
||||||
const targetPosition = this.getTargetPosition(props);
|
const targetPosition = this.getTargetPosition(props);
|
||||||
|
PropHpUIC.showFor(props);
|
||||||
Player.startAutoAttack(
|
Player.startAutoAttack(
|
||||||
() => this.onAttackStrike(),
|
() => this.onAttackStrike(),
|
||||||
() => {},
|
() => {},
|
||||||
|
|||||||
@@ -57,7 +57,12 @@ export class InteractiveZoneC {
|
|||||||
if ("color" in cloned) {
|
if ("color" in cloned) {
|
||||||
this.baseColors.set(cloned, (cloned.color as Color).clone());
|
this.baseColors.set(cloned, (cloned.color as Color).clone());
|
||||||
}
|
}
|
||||||
|
cloned.depthWrite = true;
|
||||||
|
cloned.polygonOffset = true;
|
||||||
|
cloned.polygonOffsetFactor = 2;
|
||||||
|
cloned.polygonOffsetUnits = 2;
|
||||||
mesh.material = cloned;
|
mesh.material = cloned;
|
||||||
|
mesh.renderOrder = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.refreshFillVisual();
|
this.refreshFillVisual();
|
||||||
|
|||||||
@@ -22,12 +22,18 @@ export class Map {
|
|||||||
mapObject.position.set(0, 0, 0);
|
mapObject.position.set(0, 0, 0);
|
||||||
ThreeC.setShadowsStateForChildren(mapObject, true, true);
|
ThreeC.setShadowsStateForChildren(mapObject, true, true);
|
||||||
ThreeC.addToScene(mapObject);
|
ThreeC.addToScene(mapObject);
|
||||||
|
this.hideReferenceHpUi(mapObject);
|
||||||
|
|
||||||
PhysicsTriggerC.init();
|
PhysicsTriggerC.init();
|
||||||
this.buildPhysics(mapObject);
|
this.buildPhysics(mapObject);
|
||||||
InteractiveZoneC.init(mapObject);
|
InteractiveZoneC.init(mapObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static hideReferenceHpUi(mapObject: Object3D) {
|
||||||
|
const ui = mapObject.getObjectByName("UI");
|
||||||
|
if (ui) ui.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
private static buildPhysics(mapObject: Object3D) {
|
private static buildPhysics(mapObject: Object3D) {
|
||||||
for (const layerName of MAP_PHYSICS_LAYERS) {
|
for (const layerName of MAP_PHYSICS_LAYERS) {
|
||||||
const layer = mapObject.getObjectByName(layerName);
|
const layer = mapObject.getObjectByName(layerName);
|
||||||
|
|||||||
@@ -1,42 +1,108 @@
|
|||||||
import { Box3, Mesh, Object3D, Vector3 } from "three";
|
import {
|
||||||
import { CameraC } from "../CameraC";
|
Box3,
|
||||||
|
Color,
|
||||||
|
DoubleSide,
|
||||||
|
Material,
|
||||||
|
Matrix4,
|
||||||
|
Mesh,
|
||||||
|
MeshBasicMaterial,
|
||||||
|
MeshStandardMaterial,
|
||||||
|
Object3D,
|
||||||
|
Quaternion,
|
||||||
|
Vector3,
|
||||||
|
} from "three";
|
||||||
|
import { ResourcesC } from "@24tools/playable_template";
|
||||||
|
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
|
||||||
|
import { ThreeC } from "../ThreeC";
|
||||||
|
import { ResourcesType } from "../Presets/Enums/ResourcesType";
|
||||||
import { PropC } from "./PropC";
|
import { PropC } from "./PropC";
|
||||||
|
|
||||||
const BAR_Y_OFFSET = 0.8;
|
const HP_BAR_MESH = "hpBar";
|
||||||
const BAR_X_OFFSET = -0.3;
|
const NODE_UI = "UI";
|
||||||
|
const NODE_BACKGROUND = "UI_Background";
|
||||||
|
const NODE_FOREGROUND = "UI_Foreground";
|
||||||
|
const NODE_MIDDLEGROUND = "UI_Middleground";
|
||||||
|
|
||||||
|
const BAR_Y_OFFSET = 0.55;
|
||||||
|
const BAR_X_OFFSET = -0.2;
|
||||||
const CATCHUP_SPEED = 2.5;
|
const CATCHUP_SPEED = 2.5;
|
||||||
|
const BAR_WORLD_SCALE = 1;
|
||||||
|
|
||||||
|
/** Transparent pass + renderOrder above all map decals. */
|
||||||
|
export const OVERLAY_RENDER_ORDER = 10000;
|
||||||
|
const RENDER_ORDER_BACKGROUND = OVERLAY_RENDER_ORDER;
|
||||||
|
const RENDER_ORDER_MIDDLEGROUND = OVERLAY_RENDER_ORDER + 1;
|
||||||
|
const RENDER_ORDER_FOREGROUND = OVERLAY_RENDER_ORDER + 2;
|
||||||
|
|
||||||
|
type FillPart = {
|
||||||
|
node: Object3D;
|
||||||
|
baseScaleX: number;
|
||||||
|
basePosX: number;
|
||||||
|
/** Local mesh min.x (negative); keeps left edge fixed when scaling. */
|
||||||
|
anchorMinX: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class PropHpBar {
|
export class PropHpBar {
|
||||||
|
static readonly OVERLAY_RENDER_ORDER = OVERLAY_RENDER_ORDER;
|
||||||
|
private static readonly identityMatrix = new Matrix4();
|
||||||
|
private static readonly tempMatrix = new Matrix4();
|
||||||
|
private static readonly tempPosition = new Vector3();
|
||||||
|
private static readonly tempQuaternion = new Quaternion();
|
||||||
|
private static readonly tempScale = new Vector3();
|
||||||
|
private static readonly worldAnchor = new Vector3();
|
||||||
|
|
||||||
private readonly prop: PropC;
|
private readonly prop: PropC;
|
||||||
private readonly root: HTMLElement;
|
private readonly root: Object3D;
|
||||||
private readonly currentEl: HTMLElement;
|
private readonly foreground: FillPart | null;
|
||||||
private readonly delayedEl: HTMLElement;
|
private readonly middleground: FillPart | null;
|
||||||
private readonly anchor = new Vector3();
|
|
||||||
private readonly bounds = new Box3();
|
private readonly bounds = new Box3();
|
||||||
|
|
||||||
private displayedHealth: number;
|
private displayedHealth: number;
|
||||||
private visible = false;
|
private visible = false;
|
||||||
|
|
||||||
constructor(prop: PropC, parent: HTMLElement) {
|
static getPrefab(): Object3D | null {
|
||||||
|
const fromScene = ThreeC.getObject(HP_BAR_MESH);
|
||||||
|
if (fromScene) return fromScene;
|
||||||
|
|
||||||
|
const gltf = ResourcesC.getResource<GLTF>(ResourcesType.Mesh, HP_BAR_MESH);
|
||||||
|
return gltf?.scene ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(prop: PropC, parent: Object3D) {
|
||||||
|
const template = PropHpBar.getPrefab();
|
||||||
|
if (!template) {
|
||||||
|
throw new Error(`HP bar mesh resource not found: ${HP_BAR_MESH}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceUi = template.getObjectByName(NODE_UI);
|
||||||
|
if (!sourceUi) {
|
||||||
|
throw new Error(`HP bar node not found: ${NODE_UI}`);
|
||||||
|
}
|
||||||
|
|
||||||
this.prop = prop;
|
this.prop = prop;
|
||||||
this.displayedHealth = prop.health;
|
this.displayedHealth = prop.health;
|
||||||
|
|
||||||
this.root = document.createElement("div");
|
this.root = new Object3D();
|
||||||
this.root.className = "prop-hp-bar";
|
this.root.visible = false;
|
||||||
|
this.root.scale.setScalar(BAR_WORLD_SCALE);
|
||||||
|
this.root.frustumCulled = false;
|
||||||
|
this.root.renderOrder = RENDER_ORDER_FOREGROUND;
|
||||||
|
|
||||||
const track = document.createElement("div");
|
const ui = sourceUi.clone(true);
|
||||||
track.className = "prop-hp-bar__track";
|
this.bakeMatrices(ui);
|
||||||
|
this.centerGroup(ui);
|
||||||
|
this.prepareMeshes(ui);
|
||||||
|
|
||||||
this.delayedEl = document.createElement("div");
|
this.root.add(ui);
|
||||||
this.delayedEl.className = "prop-hp-bar__delayed";
|
parent.add(this.root);
|
||||||
|
|
||||||
this.currentEl = document.createElement("div");
|
const fgNode = ui.getObjectByName(NODE_FOREGROUND);
|
||||||
this.currentEl.className = "prop-hp-bar__current";
|
const mgNode = ui.getObjectByName(NODE_MIDDLEGROUND);
|
||||||
|
this.foreground = fgNode ? this.createFillPart(fgNode) : null;
|
||||||
|
this.middleground = mgNode ? this.createFillPart(mgNode) : null;
|
||||||
|
|
||||||
track.appendChild(this.delayedEl);
|
const background = ui.getObjectByName(NODE_BACKGROUND);
|
||||||
track.appendChild(this.currentEl);
|
if (background) background.visible = true;
|
||||||
this.root.appendChild(track);
|
|
||||||
parent.appendChild(this.root);
|
|
||||||
|
|
||||||
this.applyWidths();
|
this.applyWidths();
|
||||||
}
|
}
|
||||||
@@ -61,32 +127,68 @@ export class PropHpBar {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.delayedEl.style.width = `${this.getHealthRatio(this.displayedHealth) * 100}%`;
|
if (this.middleground) {
|
||||||
|
this.applyFill(this.middleground, this.getHealthRatio(this.displayedHealth));
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.visible) return;
|
if (!this.visible) return;
|
||||||
|
|
||||||
this.updatePosition();
|
this.updateTransform();
|
||||||
}
|
}
|
||||||
|
|
||||||
setVisible(value: boolean) {
|
setVisible(value: boolean) {
|
||||||
this.visible = value;
|
this.visible = value;
|
||||||
this.root.classList.toggle("is-visible", value);
|
this.root.visible = value;
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
this.displayedHealth = this.prop.health;
|
this.displayedHealth = this.prop.health;
|
||||||
this.applyWidths();
|
this.applyWidths();
|
||||||
this.updatePosition();
|
this.updateTransform();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.root.remove();
|
this.root.removeFromParent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyWidths() {
|
private applyWidths() {
|
||||||
const ratio = this.getHealthRatio(this.prop.health);
|
if (this.foreground) {
|
||||||
this.currentEl.style.width = `${ratio * 100}%`;
|
this.applyFill(this.foreground, this.getHealthRatio(this.prop.health));
|
||||||
this.delayedEl.style.width = `${this.getHealthRatio(this.displayedHealth) * 100}%`;
|
}
|
||||||
|
if (this.middleground) {
|
||||||
|
this.applyFill(this.middleground, this.getHealthRatio(this.displayedHealth));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyFill(part: FillPart, ratio: number) {
|
||||||
|
const clamped = Math.max(0, Math.min(1, ratio));
|
||||||
|
part.node.scale.x = part.baseScaleX * clamped;
|
||||||
|
part.node.position.x =
|
||||||
|
part.basePosX + part.anchorMinX * part.baseScaleX * (1 - clamped);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createFillPart(node: Object3D): FillPart {
|
||||||
|
const meshMinX = this.getMeshMinX(node);
|
||||||
|
const isForeground = node.name.includes(NODE_FOREGROUND);
|
||||||
|
|
||||||
|
node.renderOrder = isForeground
|
||||||
|
? RENDER_ORDER_FOREGROUND
|
||||||
|
: RENDER_ORDER_MIDDLEGROUND;
|
||||||
|
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
baseScaleX: node.scale.x,
|
||||||
|
basePosX: node.position.x,
|
||||||
|
anchorMinX: meshMinX,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMeshMinX(node: Object3D) {
|
||||||
|
const mesh = node as Mesh;
|
||||||
|
if (!mesh.isMesh || !mesh.geometry) return -0.935;
|
||||||
|
|
||||||
|
mesh.geometry.computeBoundingBox();
|
||||||
|
return mesh.geometry.boundingBox?.min.x ?? -0.935;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getHealthRatio(health: number) {
|
private getHealthRatio(health: number) {
|
||||||
@@ -94,78 +196,138 @@ export class PropHpBar {
|
|||||||
return Math.max(health, 0) / this.prop.maxHealth;
|
return Math.max(health, 0) / this.prop.maxHealth;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updatePosition() {
|
private updateTransform() {
|
||||||
if (!this.computeVisualBounds()) {
|
if (!this.computeVisualBounds()) {
|
||||||
this.root.style.display = "none";
|
this.root.visible = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bounds.getCenter(this.anchor);
|
PropHpBar.worldAnchor.set(
|
||||||
this.anchor.x += BAR_X_OFFSET;
|
(this.bounds.min.x + this.bounds.max.x) * 0.5 + BAR_X_OFFSET,
|
||||||
this.anchor.y = this.bounds.max.y + BAR_Y_OFFSET;
|
this.bounds.max.y + BAR_Y_OFFSET,
|
||||||
|
(this.bounds.min.z + this.bounds.max.z) * 0.5,
|
||||||
|
);
|
||||||
|
|
||||||
const camera = CameraC.camera;
|
this.root.position.copy(PropHpBar.worldAnchor);
|
||||||
const projected = this.anchor.project(camera);
|
this.root.quaternion.set(0, 0, 0, 1);
|
||||||
|
this.root.visible = this.visible;
|
||||||
if (projected.z > 1) {
|
|
||||||
this.root.style.display = "none";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const screenPos = PropHpBar.worldToLayerPosition(projected, this.root.parentElement);
|
|
||||||
if (!screenPos) {
|
|
||||||
this.root.style.display = "none";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.root.style.display = "";
|
|
||||||
this.root.style.left = `${screenPos.x}px`;
|
|
||||||
this.root.style.top = `${screenPos.y}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static worldToLayerPosition(
|
|
||||||
projected: Vector3,
|
|
||||||
layer: HTMLElement | null,
|
|
||||||
): { x: number; y: number } | null {
|
|
||||||
const canvas = document.querySelector("canvas");
|
|
||||||
if (!canvas || !layer) return null;
|
|
||||||
|
|
||||||
const canvasRect = canvas.getBoundingClientRect();
|
|
||||||
const layerRect = layer.getBoundingClientRect();
|
|
||||||
|
|
||||||
return {
|
|
||||||
x:
|
|
||||||
(projected.x * 0.5 + 0.5) * canvasRect.width +
|
|
||||||
canvasRect.left -
|
|
||||||
layerRect.left,
|
|
||||||
y:
|
|
||||||
(-projected.y * 0.5 + 0.5) * canvasRect.height +
|
|
||||||
canvasRect.top -
|
|
||||||
layerRect.top,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private computeVisualBounds() {
|
private computeVisualBounds() {
|
||||||
this.prop.object.updateWorldMatrix(true, true);
|
this.prop.object.updateWorldMatrix(true, true);
|
||||||
this.bounds.makeEmpty();
|
this.bounds.makeEmpty();
|
||||||
|
|
||||||
const includeMesh = (node: Object3D) => {
|
this.prop.object.traverse((child) => {
|
||||||
if (!(node as Mesh).isMesh) return;
|
if (!(child as Mesh).isMesh) return;
|
||||||
|
|
||||||
const name = node.name.toLowerCase();
|
const name = child.name.toLowerCase();
|
||||||
if (name.includes("collider") || name.includes("trigger") || name.includes("gathertrigger")) {
|
if (name.includes("collider") || name.includes("trigger") || name.includes("gathertrigger")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bounds.expandByObject(node);
|
this.bounds.expandByObject(child);
|
||||||
};
|
});
|
||||||
|
|
||||||
includeMesh(this.prop.object);
|
|
||||||
this.prop.object.traverse((child) => includeMesh(child));
|
|
||||||
|
|
||||||
if (!this.bounds.isEmpty()) return true;
|
if (!this.bounds.isEmpty()) return true;
|
||||||
|
|
||||||
this.bounds.setFromObject(this.prop.object);
|
this.bounds.setFromObject(this.prop.object);
|
||||||
return !this.bounds.isEmpty();
|
return !this.bounds.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bakeMatrices(ui: Object3D) {
|
||||||
|
this.decomposeNodeMatrix(ui);
|
||||||
|
ui.traverse((child) => {
|
||||||
|
if (child === ui) return;
|
||||||
|
this.decomposeNodeMatrix(child);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private decomposeNodeMatrix(node: Object3D) {
|
||||||
|
node.updateMatrix();
|
||||||
|
PropHpBar.tempMatrix.copy(node.matrix);
|
||||||
|
if (PropHpBar.tempMatrix.equals(PropHpBar.identityMatrix)) return;
|
||||||
|
|
||||||
|
PropHpBar.tempMatrix.decompose(
|
||||||
|
PropHpBar.tempPosition,
|
||||||
|
PropHpBar.tempQuaternion,
|
||||||
|
PropHpBar.tempScale,
|
||||||
|
);
|
||||||
|
|
||||||
|
node.matrix.identity();
|
||||||
|
node.position.copy(PropHpBar.tempPosition);
|
||||||
|
node.quaternion.copy(PropHpBar.tempQuaternion);
|
||||||
|
node.scale.copy(PropHpBar.tempScale);
|
||||||
|
node.matrixAutoUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private centerGroup(group: Object3D) {
|
||||||
|
const box = new Box3().setFromObject(group);
|
||||||
|
const center = new Vector3();
|
||||||
|
box.getCenter(center);
|
||||||
|
group.position.sub(center);
|
||||||
|
}
|
||||||
|
|
||||||
|
private prepareMeshes(root: Object3D) {
|
||||||
|
root.traverse((child) => {
|
||||||
|
child.frustumCulled = false;
|
||||||
|
|
||||||
|
if (!(child as Mesh).isMesh) return;
|
||||||
|
|
||||||
|
const mesh = child as Mesh;
|
||||||
|
mesh.castShadow = false;
|
||||||
|
mesh.receiveShadow = false;
|
||||||
|
|
||||||
|
if (child.name.includes(NODE_BACKGROUND)) {
|
||||||
|
child.renderOrder = RENDER_ORDER_BACKGROUND;
|
||||||
|
} else if (child.name.includes(NODE_MIDDLEGROUND)) {
|
||||||
|
child.renderOrder = RENDER_ORDER_MIDDLEGROUND;
|
||||||
|
} else if (child.name.includes(NODE_FOREGROUND)) {
|
||||||
|
child.renderOrder = RENDER_ORDER_FOREGROUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(mesh.material)) {
|
||||||
|
mesh.material = mesh.material.map((material) => this.toVisibleMaterial(material));
|
||||||
|
} else {
|
||||||
|
mesh.material = this.toVisibleMaterial(mesh.material);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keeps GLB map/color; unlit basic material renders reliably in playable. */
|
||||||
|
private toVisibleMaterial(source: Material) {
|
||||||
|
if (source instanceof MeshBasicMaterial) {
|
||||||
|
const cloned = source.clone();
|
||||||
|
cloned.side = DoubleSide;
|
||||||
|
cloned.depthTest = false;
|
||||||
|
cloned.depthWrite = false;
|
||||||
|
cloned.toneMapped = false;
|
||||||
|
cloned.transparent = true;
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
const src = source as MeshStandardMaterial;
|
||||||
|
const material = new MeshBasicMaterial({
|
||||||
|
side: DoubleSide,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false,
|
||||||
|
toneMapped: false,
|
||||||
|
transparent: true,
|
||||||
|
opacity: src.opacity ?? 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (src.map) {
|
||||||
|
material.map = src.map;
|
||||||
|
}
|
||||||
|
if (src.color) {
|
||||||
|
material.color.copy(src.color);
|
||||||
|
} else {
|
||||||
|
material.color = new Color(0xffffff);
|
||||||
|
}
|
||||||
|
if (src.alphaTest) {
|
||||||
|
material.alphaTest = src.alphaTest;
|
||||||
|
material.transparent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return material;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { Object3D } from "three";
|
||||||
import { UpdateController } from "@24tools/playable_template";
|
import { UpdateController } from "@24tools/playable_template";
|
||||||
|
import { ThreeC } from "../ThreeC";
|
||||||
import { PropC } from "./PropC";
|
import { PropC } from "./PropC";
|
||||||
import { PropHpBar } from "./PropHpBar";
|
import { PropHpBar } from "./PropHpBar";
|
||||||
|
|
||||||
@@ -7,20 +9,15 @@ const DEBUG_ALWAYS_VISIBLE_HP_BARS = false;
|
|||||||
|
|
||||||
export class PropHpUIC {
|
export class PropHpUIC {
|
||||||
private static inited = false;
|
private static inited = false;
|
||||||
private static root: HTMLElement | null = null;
|
private static overlayLayer: Object3D | null = null;
|
||||||
private static bars = new Map<PropC, PropHpBar>();
|
private static bars = new Map<PropC, PropHpBar>();
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
if (this.inited) return;
|
if (this.inited) return;
|
||||||
this.inited = true;
|
this.inited = true;
|
||||||
|
|
||||||
const uiRoot = document.getElementById("ui");
|
this.hideSceneTemplate();
|
||||||
if (!uiRoot) return;
|
this.ensureOverlayLayer();
|
||||||
|
|
||||||
this.root = document.createElement("div");
|
|
||||||
this.root.id = "prop-hp-layer";
|
|
||||||
this.root.className = "prop-hp-layer";
|
|
||||||
uiRoot.appendChild(this.root);
|
|
||||||
|
|
||||||
UpdateController.Instance.onUpdate.addDelegate((delta) => {
|
UpdateController.Instance.onUpdate.addDelegate((delta) => {
|
||||||
for (const bar of this.bars.values()) {
|
for (const bar of this.bars.values()) {
|
||||||
@@ -29,10 +26,31 @@ export class PropHpUIC {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static createBar(prop: PropC): PropHpBar | undefined {
|
/** Call after map/scene objects are added so overlay renders last. */
|
||||||
if (!this.root || this.bars.has(prop)) return this.bars.get(prop);
|
static bringOverlayToFront() {
|
||||||
|
this.ensureOverlayLayer();
|
||||||
|
const layer = this.overlayLayer;
|
||||||
|
if (!layer) return;
|
||||||
|
|
||||||
const bar = new PropHpBar(prop, this.root);
|
let parent = layer.parent;
|
||||||
|
if (!parent) {
|
||||||
|
ThreeC.addToScene(layer);
|
||||||
|
parent = layer.parent;
|
||||||
|
if (!parent) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.remove(layer);
|
||||||
|
parent.add(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static createBar(prop: PropC): PropHpBar | undefined {
|
||||||
|
if (!PropHpBar.getPrefab()) {
|
||||||
|
console.warn("HP bar prefab not loaded");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (this.bars.has(prop)) return this.bars.get(prop);
|
||||||
|
|
||||||
|
const bar = new PropHpBar(prop, this.getOverlayLayer());
|
||||||
this.bars.set(prop, bar);
|
this.bars.set(prop, bar);
|
||||||
|
|
||||||
if (DEBUG_ALWAYS_VISIBLE_HP_BARS) {
|
if (DEBUG_ALWAYS_VISIBLE_HP_BARS) {
|
||||||
@@ -79,4 +97,27 @@ export class PropHpUIC {
|
|||||||
bar.destroy();
|
bar.destroy();
|
||||||
this.bars.delete(prop);
|
this.bars.delete(prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static hideSceneTemplate() {
|
||||||
|
const template = PropHpBar.getPrefab();
|
||||||
|
if (!template || !template.parent) return;
|
||||||
|
|
||||||
|
template.visible = false;
|
||||||
|
template.removeFromParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ensureOverlayLayer() {
|
||||||
|
if (!this.overlayLayer) {
|
||||||
|
this.overlayLayer = new Object3D();
|
||||||
|
this.overlayLayer.name = "prop-hp-overlay";
|
||||||
|
this.overlayLayer.frustumCulled = false;
|
||||||
|
this.overlayLayer.renderOrder = PropHpBar.OVERLAY_RENDER_ORDER;
|
||||||
|
ThreeC.addToScene(this.overlayLayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getOverlayLayer() {
|
||||||
|
this.ensureOverlayLayer();
|
||||||
|
return this.overlayLayer!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,21 @@ import { ResourceDepositC } from "./Resources/ResourceDepositC";
|
|||||||
import { PropHpUIC } from "./Map/PropHpUIC";
|
import { PropHpUIC } from "./Map/PropHpUIC";
|
||||||
import { PropVfxC } from "./Map/PropVfxC";
|
import { PropVfxC } from "./Map/PropVfxC";
|
||||||
import { InvasionProgressUIC } from "./UI/InvasionProgressUIC";
|
import { InvasionProgressUIC } from "./UI/InvasionProgressUIC";
|
||||||
|
import { PreGameplayUIC } from "./UI/PreGameplayUIC";
|
||||||
|
|
||||||
export class TestSceneC {
|
export class TestSceneC {
|
||||||
static init() {
|
static init() {
|
||||||
InvasionProgressUIC.init();
|
InvasionProgressUIC.init();
|
||||||
ResourceUIC.init();
|
ResourceUIC.init();
|
||||||
|
PreGameplayUIC.init();
|
||||||
ResourceFlyC.init();
|
ResourceFlyC.init();
|
||||||
PropHpUIC.init();
|
PropHpUIC.init();
|
||||||
PropVfxC.Init();
|
PropVfxC.Init();
|
||||||
Map.init();
|
Map.init();
|
||||||
|
PropHpUIC.bringOverlayToFront();
|
||||||
GatherC.init();
|
GatherC.init();
|
||||||
Player.init();
|
Player.init();
|
||||||
|
PropHpUIC.bringOverlayToFront();
|
||||||
|
|
||||||
const mapObject = ThreeC.getObject("map");
|
const mapObject = ThreeC.getObject("map");
|
||||||
if (mapObject) {
|
if (mapObject) {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
export class PreGameplayUIC {
|
||||||
|
private static root: HTMLElement | null = null;
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
const ui = document.getElementById("ui");
|
||||||
|
if (!ui) return;
|
||||||
|
|
||||||
|
ui.classList.add("is-pre-gameplay");
|
||||||
|
|
||||||
|
const root = document.createElement("div");
|
||||||
|
root.className = "pre-gameplay";
|
||||||
|
|
||||||
|
const hurry = document.createElement("p");
|
||||||
|
hurry.className = "pre-gameplay__hurry";
|
||||||
|
hurry.textContent = "HURRY UP";
|
||||||
|
|
||||||
|
const bottom = document.createElement("div");
|
||||||
|
bottom.className = "pre-gameplay__bottom";
|
||||||
|
|
||||||
|
const drag = document.createElement("p");
|
||||||
|
drag.className = "pre-gameplay__drag";
|
||||||
|
drag.textContent = "DRAG TO MOVE";
|
||||||
|
|
||||||
|
const joystick = document.createElement("div");
|
||||||
|
joystick.className = "pre-gameplay-joystick";
|
||||||
|
|
||||||
|
const base = document.createElement("div");
|
||||||
|
base.className = "pre-gameplay-joystick__base";
|
||||||
|
|
||||||
|
const knob = document.createElement("div");
|
||||||
|
knob.className = "pre-gameplay-joystick__knob";
|
||||||
|
|
||||||
|
joystick.append(base, knob);
|
||||||
|
bottom.append(drag, joystick);
|
||||||
|
root.append(hurry, bottom);
|
||||||
|
ui.appendChild(root);
|
||||||
|
this.root = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
static dismiss() {
|
||||||
|
document.getElementById("ui")?.classList.remove("is-pre-gameplay");
|
||||||
|
this.root?.remove();
|
||||||
|
this.root = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#ui.is-pre-gameplay .hud-invasion-title,
|
||||||
|
#ui.is-pre-gameplay .hud-progress,
|
||||||
|
#ui.is-pre-gameplay .hud-top-right {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-gameplay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-gameplay__hurry {
|
||||||
|
position: absolute;
|
||||||
|
top: 14vh;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 5vh;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-text-stroke: 0.14vh #000000;
|
||||||
|
paint-order: stroke fill;
|
||||||
|
animation: pre-gameplay-pulse 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-gameplay__bottom {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 10vh;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2.5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-gameplay__drag {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 4.2vh;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-text-stroke: 0.12vh #000000;
|
||||||
|
paint-order: stroke fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-gameplay-joystick {
|
||||||
|
position: relative;
|
||||||
|
width: 22vh;
|
||||||
|
height: 22vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-gameplay-joystick__base {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0.35vh solid rgba(255, 255, 255, 0.55);
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-gameplay-joystick__knob {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 8vh;
|
||||||
|
height: 8vh;
|
||||||
|
margin-left: -4vh;
|
||||||
|
margin-top: -4vh;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ffffff;
|
||||||
|
animation: pre-gameplay-joystick-knob 2.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pre-gameplay-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateX(-50%) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(-50%) scale(1.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pre-gameplay-joystick-knob {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translate(5vh, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(0, -3.5vh);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translate(-5vh, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
.pre-gameplay__hurry {
|
||||||
|
top: 8vh;
|
||||||
|
font-size: 4vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-gameplay__bottom {
|
||||||
|
bottom: 6vh;
|
||||||
|
gap: 1.5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-gameplay__drag {
|
||||||
|
font-size: 3.2vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-gameplay-joystick {
|
||||||
|
width: 18vh;
|
||||||
|
height: 18vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-gameplay-joystick__knob {
|
||||||
|
width: 6.5vh;
|
||||||
|
height: 6.5vh;
|
||||||
|
margin-left: -3.25vh;
|
||||||
|
margin-top: -3.25vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pre-gameplay-joystick-knob {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translate(4vh, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(0, -2.8vh);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translate(-4vh, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -184,58 +184,6 @@
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prop-hp-layer {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prop-hp-bar {
|
|
||||||
position: absolute;
|
|
||||||
width: 8vh;
|
|
||||||
height: 1.2vh;
|
|
||||||
transform: translate(-50%, -100%);
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 15;
|
|
||||||
transition: opacity 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prop-hp-bar.is-visible {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prop-hp-bar__track {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: #000000;
|
|
||||||
border-radius: 0.25vh;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prop-hp-bar__delayed,
|
|
||||||
.prop-hp-bar__current {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 0.25vh;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prop-hp-bar__delayed {
|
|
||||||
background: #ffffff;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prop-hp-bar__current {
|
|
||||||
background: #3b8bff;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
@media (orientation: landscape) {
|
||||||
.hud-top-center {
|
.hud-top-center {
|
||||||
top: 1.2vh;
|
top: 1.2vh;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<link href="./css/loader.css" rel="stylesheet" />
|
<link href="./css/loader.css" rel="stylesheet" />
|
||||||
<link href="./css/main.css" rel="stylesheet" />
|
<link href="./css/main.css" rel="stylesheet" />
|
||||||
<link href="./css/ui.css" rel="stylesheet" />
|
<link href="./css/ui.css" rel="stylesheet" />
|
||||||
|
<link href="./css/preGameplay.css" rel="stylesheet" />
|
||||||
<style id="animations"></style>
|
<style id="animations"></style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Binary file not shown.
@@ -17,6 +17,10 @@ export const meshes : ConvertResourceType = {
|
|||||||
name: "wood",
|
name: "wood",
|
||||||
value: ConvertToBase64WhenRelease("./wood.glb"),
|
value: ConvertToBase64WhenRelease("./wood.glb"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "hpBar",
|
||||||
|
value: ConvertToBase64WhenRelease("./hpBar.glb"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
loader: Template3d.meshLoader
|
loader: Template3d.meshLoader
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { InvasionProgressUIC } from "../controllers/UI/InvasionProgressUIC";
|
import { InvasionProgressUIC } from "../controllers/UI/InvasionProgressUIC";
|
||||||
|
import { PreGameplayUIC } from "../controllers/UI/PreGameplayUIC";
|
||||||
|
|
||||||
export const firstClickCb: () => void = () => {
|
export const firstClickCb: () => void = () => {
|
||||||
|
PreGameplayUIC.dismiss();
|
||||||
InvasionProgressUIC.startTimer();
|
InvasionProgressUIC.startTimer();
|
||||||
};
|
};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user