animation, gathering, deposit, ui
@@ -21,7 +21,8 @@
|
|||||||
"cannon-es-debugger": "^1.0.0",
|
"cannon-es-debugger": "^1.0.0",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
"nipplejs": "^1.0.3",
|
"nipplejs": "^1.0.3",
|
||||||
"three": "^0.184.0"
|
"three": "^0.184.0",
|
||||||
|
"three.quarks": "^0.17.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/howler": "^2.2.13",
|
"@types/howler": "^2.2.13",
|
||||||
|
|||||||
@@ -43,17 +43,17 @@ export const globalSettings: ConfigUiParamsCategories[] = [
|
|||||||
[
|
[
|
||||||
-10,
|
-10,
|
||||||
10,
|
10,
|
||||||
6
|
-4
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
8,
|
15,
|
||||||
9
|
11
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-10,
|
-10,
|
||||||
10,
|
10,
|
||||||
7
|
-7
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -67,7 +67,7 @@ export const globalSettings: ConfigUiParamsCategories[] = [
|
|||||||
[
|
[
|
||||||
-360,
|
-360,
|
||||||
360,
|
360,
|
||||||
-5
|
0
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-360,
|
-360,
|
||||||
@@ -88,7 +88,7 @@ export const globalSettings: ConfigUiParamsCategories[] = [
|
|||||||
values: [
|
values: [
|
||||||
30,
|
30,
|
||||||
150,
|
150,
|
||||||
55
|
30
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -101,17 +101,17 @@ export const globalSettings: ConfigUiParamsCategories[] = [
|
|||||||
[
|
[
|
||||||
-10,
|
-10,
|
||||||
10,
|
10,
|
||||||
2
|
-4
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
8,
|
15,
|
||||||
5
|
9
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-10,
|
-10,
|
||||||
10,
|
10,
|
||||||
3
|
-7
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -125,7 +125,7 @@ export const globalSettings: ConfigUiParamsCategories[] = [
|
|||||||
[
|
[
|
||||||
-360,
|
-360,
|
||||||
360,
|
360,
|
||||||
0
|
5
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-360,
|
-360,
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum VFXType {
|
||||||
|
LootableHit = "lootable_hit",
|
||||||
|
LootableDestroy = "lootable_destroy",
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { EasyEvent, UpdateController } from "@24tools/playable_template";
|
||||||
|
import { Box3, Object3D, Vector3 } from "three";
|
||||||
|
import { Player } from "../Presets/Player";
|
||||||
|
import {
|
||||||
|
findInteractiveZoneFromMap,
|
||||||
|
INTERACTIVE_ZONE_NAME,
|
||||||
|
isInsideInteractiveZoneXZ,
|
||||||
|
refreshInteractiveZoneBounds,
|
||||||
|
} from "./MapInteractiveZone";
|
||||||
|
|
||||||
|
export class DepositZoneC {
|
||||||
|
static readonly onPlayerEnter = new EasyEvent<{}>();
|
||||||
|
static readonly onPlayerExit = new EasyEvent<{}>();
|
||||||
|
|
||||||
|
private static inited = false;
|
||||||
|
private static zone: Object3D | null = null;
|
||||||
|
private static bounds = new Box3();
|
||||||
|
private static playerInside = false;
|
||||||
|
private static playerPosition = new Vector3();
|
||||||
|
|
||||||
|
static init(mapObject: Object3D) {
|
||||||
|
if (this.inited) return;
|
||||||
|
|
||||||
|
this.zone = findInteractiveZoneFromMap(mapObject);
|
||||||
|
if (!this.zone) {
|
||||||
|
console.warn(`Deposit zone not found: ${INTERACTIVE_ZONE_NAME}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inited = true;
|
||||||
|
refreshInteractiveZoneBounds(this.zone, this.bounds);
|
||||||
|
|
||||||
|
UpdateController.Instance.onUpdate.addDelegate(() => {
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get isPlayerInside() {
|
||||||
|
return this.playerInside;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static update() {
|
||||||
|
this.getPlayerPosition(this.playerPosition);
|
||||||
|
refreshInteractiveZoneBounds(this.zone!, this.bounds);
|
||||||
|
this.updatePlayerInside(this.playerPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getPlayerPosition(out: Vector3) {
|
||||||
|
const body = Player.physics?.getPhysicsBody();
|
||||||
|
if (body) {
|
||||||
|
return out.set(body.position.x, body.position.y, body.position.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Player.getWorldPosition().copy(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static updatePlayerInside(playerPosition: Vector3) {
|
||||||
|
const inside = isInsideInteractiveZoneXZ(this.bounds, playerPosition);
|
||||||
|
if (inside === this.playerInside) return;
|
||||||
|
|
||||||
|
this.playerInside = inside;
|
||||||
|
if (inside) {
|
||||||
|
console.log(`[DepositZoneC] enter deposit zone: ${INTERACTIVE_ZONE_NAME}`);
|
||||||
|
this.onPlayerEnter.Invoke({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DepositZoneC] exit deposit zone: ${INTERACTIVE_ZONE_NAME}`);
|
||||||
|
this.onPlayerExit.Invoke({});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,70 +1,140 @@
|
|||||||
import { JoystickC, UpdateController } from "@24tools/playable_template";
|
import { EasyEvent, JoystickC, UpdateController } from "@24tools/playable_template";
|
||||||
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 { Player } from "../Presets/Player";
|
import { Player } from "../Presets/Player";
|
||||||
|
|
||||||
export class GatherC {
|
export class GatherC {
|
||||||
|
static readonly onCombatIdle = new EasyEvent<{}>();
|
||||||
|
|
||||||
private static activeProps = new Set<PropC>();
|
private static activeProps = new Set<PropC>();
|
||||||
private static pendingAutoAttack = false;
|
private static pendingAutoAttack = false;
|
||||||
|
private static combatIdleNotified = false;
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
|
PropC.onBroken.addDelegate(() => this.onPropDestroyed());
|
||||||
|
|
||||||
PhysicsTriggerC.onTriggerEnter.addDelegate((payload) => {
|
PhysicsTriggerC.onTriggerEnter.addDelegate((payload) => {
|
||||||
const prop = PropRegistry.get(payload.lootableObject);
|
const prop = PropRegistry.get(payload.lootableObject);
|
||||||
if (!prop || prop.isBroken) return;
|
if (!prop || prop.isBroken) return;
|
||||||
|
|
||||||
|
console.log(`[GatherC] enter gather zone: ${payload.lootableObject.name}`);
|
||||||
this.activeProps.add(prop);
|
this.activeProps.add(prop);
|
||||||
this.scheduleAutoAttackCheck();
|
this.scheduleAutoAttackCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
PhysicsTriggerC.onTriggerExit.addDelegate((payload) => {
|
PhysicsTriggerC.onTriggerExit.addDelegate((payload) => {
|
||||||
|
console.log(`[GatherC] exit gather zone: ${payload.lootableObject.name}`);
|
||||||
|
|
||||||
const prop = PropRegistry.get(payload.lootableObject);
|
const prop = PropRegistry.get(payload.lootableObject);
|
||||||
if (prop) this.activeProps.delete(prop);
|
if (prop) this.activeProps.delete(prop);
|
||||||
|
|
||||||
this.removeBrokenProps();
|
this.removeBrokenProps();
|
||||||
|
this.resyncActivePropsFromPhysics();
|
||||||
if (this.getAttackableProps().length === 0) {
|
this.handleRemainingTargets();
|
||||||
Player.stopAutoAttack();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
JoystickC.onJoysticEnd.addDelegate(() => {
|
JoystickC.onJoysticEnd.addDelegate(() => {
|
||||||
this.scheduleAutoAttackCheck();
|
this.scheduleAutoAttackCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
JoystickC.onJoysticMove.addDelegate(() => {
|
||||||
|
if (!Player.isMoving()) {
|
||||||
|
this.scheduleAutoAttackCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
UpdateController.Instance.onUpdate.addDelegate(() => {
|
UpdateController.Instance.onUpdate.addDelegate(() => {
|
||||||
if (!this.pendingAutoAttack) return;
|
if (this.pendingAutoAttack) {
|
||||||
this.pendingAutoAttack = false;
|
this.pendingAutoAttack = false;
|
||||||
this.tryStartAutoAttack();
|
this.tryStartAutoAttack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tryResumeAutoAttack();
|
||||||
|
this.notifyCombatIdleIfNeeded();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static onPropDestroyed() {
|
||||||
|
this.removeBrokenProps();
|
||||||
|
this.resyncActivePropsFromPhysics();
|
||||||
|
this.handleRemainingTargets();
|
||||||
|
}
|
||||||
|
|
||||||
private static scheduleAutoAttackCheck() {
|
private static scheduleAutoAttackCheck() {
|
||||||
this.pendingAutoAttack = true;
|
this.pendingAutoAttack = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static onAttackHit() {
|
private static onAttackStrike() {
|
||||||
for (const prop of this.getAttackableProps()) {
|
for (const prop of this.getAttackableProps()) {
|
||||||
prop.takeDamage(1);
|
prop.takeDamage(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeBrokenProps();
|
this.removeBrokenProps();
|
||||||
|
this.resyncActivePropsFromPhysics();
|
||||||
|
this.handleRemainingTargets();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.getAttackableProps().length === 0) {
|
private static tryResumeAutoAttack() {
|
||||||
|
if (this.getAttackableProps().length === 0) return;
|
||||||
|
if (Player.isAutoAttackActive() || Player.isCombatBusy()) return;
|
||||||
|
if (Player.isMoving()) return;
|
||||||
|
this.tryStartAutoAttack();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static handleRemainingTargets() {
|
||||||
|
const props = this.getAttackableProps();
|
||||||
|
if (props.length === 0) {
|
||||||
|
if (Player.isAutoAttackActive() || Player.isCombatBusy()) {
|
||||||
Player.stopAutoAttack();
|
Player.stopAutoAttack();
|
||||||
|
}
|
||||||
|
this.notifyCombatIdleIfNeeded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Player.playAttack(() => this.onAttackHit());
|
if (Player.isMoving()) {
|
||||||
|
this.scheduleAutoAttackCheck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPosition = this.getTargetPosition(props);
|
||||||
|
|
||||||
|
if (Player.isAutoAttackActive()) {
|
||||||
|
Player.retargetAutoAttack(targetPosition);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Player.isCombatBusy()) {
|
||||||
|
this.scheduleAutoAttackCheck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tryStartAutoAttack();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static tryStartAutoAttack() {
|
private static tryStartAutoAttack() {
|
||||||
const props = this.getAttackableProps();
|
const props = this.getAttackableProps();
|
||||||
if (props.length === 0) return;
|
if (props.length === 0) return;
|
||||||
if (Player.isMoving()) return;
|
if (Player.isMoving() || Player.isCombatBusy()) return;
|
||||||
|
|
||||||
const targetPosition = this.getTargetPosition(props);
|
const targetPosition = this.getTargetPosition(props);
|
||||||
Player.startAutoAttack(() => this.onAttackHit(), targetPosition);
|
Player.startAutoAttack(
|
||||||
|
() => this.onAttackStrike(),
|
||||||
|
() => {},
|
||||||
|
targetPosition,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static resyncActivePropsFromPhysics() {
|
||||||
|
this.activeProps.clear();
|
||||||
|
|
||||||
|
for (const lootableObject of PhysicsTriggerC.getActiveLootableObjects()) {
|
||||||
|
const prop = PropRegistry.get(lootableObject);
|
||||||
|
if (prop && !prop.isBroken) {
|
||||||
|
this.activeProps.add(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getTargetPosition(props: PropC[]): Vector3 {
|
private static getTargetPosition(props: PropC[]): Vector3 {
|
||||||
@@ -93,4 +163,21 @@ export class GatherC {
|
|||||||
if (prop.isBroken) this.activeProps.delete(prop);
|
if (prop.isBroken) this.activeProps.delete(prop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static notifyCombatIdleIfNeeded() {
|
||||||
|
if (this.getAttackableProps().length > 0) {
|
||||||
|
this.combatIdleNotified = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Player.isAutoAttackActive() || Player.isCombatBusy()) {
|
||||||
|
this.combatIdleNotified = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.combatIdleNotified) return;
|
||||||
|
|
||||||
|
this.combatIdleNotified = true;
|
||||||
|
this.onCombatIdle.Invoke({});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { Color, Material, Mesh, Object3D, Vector3 } from "three";
|
||||||
|
import { ResourceScreenFly } from "../Resources/ResourceScreenFly";
|
||||||
|
import {
|
||||||
|
findInteractiveZoneFromMap,
|
||||||
|
getInteractiveZoneCenter,
|
||||||
|
INTERACTIVE_ZONE_NAME,
|
||||||
|
} from "./MapInteractiveZone";
|
||||||
|
|
||||||
|
const FILL_TARGET = 10;
|
||||||
|
const FILL_COLOR = new Color(0x2db83a);
|
||||||
|
|
||||||
|
export class InteractiveZoneC {
|
||||||
|
private static zoneRoot: Object3D | null = null;
|
||||||
|
private static deposited = 0;
|
||||||
|
private static baseColors = new WeakMap<Material, Color>();
|
||||||
|
|
||||||
|
static init(mapObject: Object3D) {
|
||||||
|
const zone = findInteractiveZoneFromMap(mapObject);
|
||||||
|
if (!zone) {
|
||||||
|
console.warn(`Interactive zone not found: ${INTERACTIVE_ZONE_NAME}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.zoneRoot = zone;
|
||||||
|
this.setupMaterials(zone);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getWorldCenter(out = new Vector3()) {
|
||||||
|
if (!this.zoneRoot) return out.set(0, 0, 0);
|
||||||
|
return getInteractiveZoneCenter(this.zoneRoot, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getScreenCenter() {
|
||||||
|
return ResourceScreenFly.worldToScreen(this.getWorldCenter());
|
||||||
|
}
|
||||||
|
|
||||||
|
static addDeposit(amount = 1) {
|
||||||
|
this.deposited += amount;
|
||||||
|
this.refreshFillVisual();
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFillRatio() {
|
||||||
|
return Math.min(this.deposited / FILL_TARGET, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static setupMaterials(zone: Object3D) {
|
||||||
|
zone.traverse((child) => {
|
||||||
|
if (!(child as Mesh).isMesh) return;
|
||||||
|
|
||||||
|
const mesh = child as Mesh;
|
||||||
|
const sourceMaterial = mesh.material;
|
||||||
|
if (Array.isArray(sourceMaterial)) return;
|
||||||
|
|
||||||
|
if (!sourceMaterial) return;
|
||||||
|
|
||||||
|
const cloned = sourceMaterial.clone();
|
||||||
|
if ("color" in cloned) {
|
||||||
|
this.baseColors.set(cloned, (cloned.color as Color).clone());
|
||||||
|
}
|
||||||
|
mesh.material = cloned;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.refreshFillVisual();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static refreshFillVisual() {
|
||||||
|
if (!this.zoneRoot) return;
|
||||||
|
|
||||||
|
const ratio = this.getFillRatio();
|
||||||
|
|
||||||
|
this.zoneRoot.traverse((child) => {
|
||||||
|
if (!(child as Mesh).isMesh) return;
|
||||||
|
|
||||||
|
const material = (child as Mesh).material;
|
||||||
|
if (Array.isArray(material) || !material) return;
|
||||||
|
|
||||||
|
const baseColor = this.baseColors.get(material);
|
||||||
|
if (!baseColor || !("color" in material)) return;
|
||||||
|
|
||||||
|
(material.color as Color).copy(baseColor).lerp(FILL_COLOR, ratio);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,13 +3,16 @@ import { ThreeC } from "../ThreeC";
|
|||||||
import { PhysicsBody, PhysicsLayer } from "../PhysicsC";
|
import { PhysicsBody, PhysicsLayer } from "../PhysicsC";
|
||||||
import { PhysicsTriggerC } from "./PhysicsTriggerC";
|
import { PhysicsTriggerC } from "./PhysicsTriggerC";
|
||||||
import { PropC } from "./PropC";
|
import { PropC } from "./PropC";
|
||||||
|
import { findDamageStateLayers } from "./PropDamageLayers";
|
||||||
import { resolvePropType } from "../Resources/PropDropTable";
|
import { resolvePropType } from "../Resources/PropDropTable";
|
||||||
|
|
||||||
|
import { InteractiveZoneC } from "./InteractiveZoneC";
|
||||||
|
|
||||||
const MAP_PHYSICS_LAYERS = ["Colliders", "Lootable"];
|
const MAP_PHYSICS_LAYERS = ["Colliders", "Lootable"];
|
||||||
const GATHER_TRIGGER_PADDING = 0.2;
|
const GATHER_TRIGGER_PADDING = 0.5;
|
||||||
|
|
||||||
export class Map {
|
export class Map {
|
||||||
static Init() {
|
static init() {
|
||||||
const mapObject = ThreeC.getObject("map");
|
const mapObject = ThreeC.getObject("map");
|
||||||
if (!mapObject) {
|
if (!mapObject) {
|
||||||
console.warn("Map model resource not found: map");
|
console.warn("Map model resource not found: map");
|
||||||
@@ -22,6 +25,7 @@ export class Map {
|
|||||||
|
|
||||||
PhysicsTriggerC.init();
|
PhysicsTriggerC.init();
|
||||||
this.buildPhysics(mapObject);
|
this.buildPhysics(mapObject);
|
||||||
|
InteractiveZoneC.init(mapObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static buildPhysics(mapObject: Object3D) {
|
private static buildPhysics(mapObject: Object3D) {
|
||||||
@@ -55,7 +59,10 @@ export class Map {
|
|||||||
|
|
||||||
private static setupLootableObject(lootableObject: Object3D) {
|
private static setupLootableObject(lootableObject: Object3D) {
|
||||||
const propType = resolvePropType(lootableObject.name);
|
const propType = resolvePropType(lootableObject.name);
|
||||||
const prop = new PropC(lootableObject, propType);
|
const damageLayers = findDamageStateLayers(lootableObject);
|
||||||
|
const maxHealth = damageLayers.length > 0 ? damageLayers.length : 1;
|
||||||
|
const prop = new PropC(lootableObject, propType, maxHealth);
|
||||||
|
prop.initDamageLayers(damageLayers);
|
||||||
const colliderMesh = this.findMesh(lootableObject, (name) => name.includes("collider"));
|
const colliderMesh = this.findMesh(lootableObject, (name) => name.includes("collider"));
|
||||||
const visualMeshes = this.getVisualMeshes(lootableObject);
|
const visualMeshes = this.getVisualMeshes(lootableObject);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Box3, Object3D, Vector3 } from "three";
|
||||||
|
|
||||||
|
export const INTERACTIVE_ZONE_NAME = "UI_Interactive_Zone_02";
|
||||||
|
const ZONE_XZ_PADDING = 0.35;
|
||||||
|
|
||||||
|
export function findInteractiveZone(root: Object3D): Object3D | null {
|
||||||
|
let found: Object3D | null = null;
|
||||||
|
|
||||||
|
root.traverse((child) => {
|
||||||
|
if (!found && child.name === INTERACTIVE_ZONE_NAME) {
|
||||||
|
found = child;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findInteractiveZoneFromMap(mapObject: Object3D): Object3D | null {
|
||||||
|
let sceneRoot: Object3D = mapObject;
|
||||||
|
while (sceneRoot.parent) {
|
||||||
|
sceneRoot = sceneRoot.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return findInteractiveZone(sceneRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshInteractiveZoneBounds(zone: Object3D, out = new Box3()) {
|
||||||
|
zone.updateWorldMatrix(true, true);
|
||||||
|
out.setFromObject(zone);
|
||||||
|
out.min.x -= ZONE_XZ_PADDING;
|
||||||
|
out.max.x += ZONE_XZ_PADDING;
|
||||||
|
out.min.z -= ZONE_XZ_PADDING;
|
||||||
|
out.max.z += ZONE_XZ_PADDING;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isInsideInteractiveZoneXZ(bounds: Box3, position: Vector3) {
|
||||||
|
return position.x >= bounds.min.x
|
||||||
|
&& position.x <= bounds.max.x
|
||||||
|
&& position.z >= bounds.min.z
|
||||||
|
&& position.z <= bounds.max.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInteractiveZoneCenter(zone: Object3D, out = new Vector3()) {
|
||||||
|
const bounds = refreshInteractiveZoneBounds(zone);
|
||||||
|
return bounds.getCenter(out);
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
EasyEvent,
|
EasyEvent,
|
||||||
Physics_internal,
|
|
||||||
UpdateController,
|
UpdateController,
|
||||||
} from "@24tools/playable_template";
|
} from "@24tools/playable_template";
|
||||||
import { Object3D } from "three";
|
import { Box3, Object3D, Vector3 } from "three";
|
||||||
import { Body, Box } from "cannon-es";
|
import { Body } from "cannon-es";
|
||||||
import { PhysicsBody, PhysicsLayer } from "../PhysicsC";
|
|
||||||
|
|
||||||
const PLAYER_RADIUS = 0.3;
|
const PLAYER_RADIUS = 0.3;
|
||||||
|
|
||||||
@@ -18,15 +16,15 @@ export type TriggerEventPayload = {
|
|||||||
type TriggerRecord = {
|
type TriggerRecord = {
|
||||||
lootableObject: Object3D;
|
lootableObject: Object3D;
|
||||||
triggerObject: Object3D;
|
triggerObject: Object3D;
|
||||||
|
center: Vector3;
|
||||||
radius: number;
|
radius: number;
|
||||||
position: Body["position"];
|
|
||||||
physicsBody: PhysicsBody;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PhysicsTriggerC {
|
export class PhysicsTriggerC {
|
||||||
private static inited = false;
|
private static inited = false;
|
||||||
private static triggers: TriggerRecord[] = [];
|
private static triggers: TriggerRecord[] = [];
|
||||||
private static activeTriggers = new Set<TriggerRecord>();
|
private static activeTriggers = new Set<TriggerRecord>();
|
||||||
|
private static playerBody: Body | null = null;
|
||||||
|
|
||||||
static onTriggerEnter = new EasyEvent<TriggerEventPayload>();
|
static onTriggerEnter = new EasyEvent<TriggerEventPayload>();
|
||||||
static onTriggerExit = new EasyEvent<TriggerEventPayload>();
|
static onTriggerExit = new EasyEvent<TriggerEventPayload>();
|
||||||
@@ -38,50 +36,62 @@ export class PhysicsTriggerC {
|
|||||||
UpdateController.Instance.onUpdate.addDelegate(() => this.update());
|
UpdateController.Instance.onUpdate.addDelegate(() => this.update());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static setPlayerBody(body: Body) {
|
||||||
|
this.playerBody = body;
|
||||||
|
}
|
||||||
|
|
||||||
static register(triggerObject: Object3D, lootableObject: Object3D) {
|
static register(triggerObject: Object3D, lootableObject: Object3D) {
|
||||||
const physicsBody = new PhysicsBody(
|
triggerObject.updateWorldMatrix(true, true);
|
||||||
triggerObject,
|
|
||||||
true,
|
|
||||||
0,
|
|
||||||
PhysicsLayer.Trigger,
|
|
||||||
PhysicsLayer.Player,
|
|
||||||
);
|
|
||||||
|
|
||||||
const body = physicsBody.getPhysicsBody();
|
const center = new Vector3();
|
||||||
body.allowSleep = false;
|
const size = new Vector3();
|
||||||
|
const bounds = new Box3().setFromObject(triggerObject);
|
||||||
const shape = body.shapes[0] as Box;
|
bounds.getCenter(center);
|
||||||
const halfExtents = shape.halfExtents;
|
bounds.getSize(size);
|
||||||
|
|
||||||
this.triggers.push({
|
this.triggers.push({
|
||||||
lootableObject,
|
lootableObject,
|
||||||
triggerObject,
|
triggerObject,
|
||||||
radius: Math.max(halfExtents.x, halfExtents.y, halfExtents.z),
|
center,
|
||||||
position: body.position,
|
radius: Math.max(size.x, size.y, size.z) / 2,
|
||||||
physicsBody,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getActiveLootableObjects(): Object3D[] {
|
||||||
|
const lootables: Object3D[] = [];
|
||||||
|
for (const record of this.activeTriggers) {
|
||||||
|
lootables.push(record.lootableObject);
|
||||||
|
}
|
||||||
|
return lootables;
|
||||||
|
}
|
||||||
|
|
||||||
static unregister(lootableObject: Object3D) {
|
static unregister(lootableObject: Object3D) {
|
||||||
|
const playerBody = this.playerBody;
|
||||||
|
|
||||||
this.triggers = this.triggers.filter((record) => {
|
this.triggers = this.triggers.filter((record) => {
|
||||||
if (record.lootableObject !== lootableObject) return true;
|
if (record.lootableObject !== lootableObject) return true;
|
||||||
|
|
||||||
record.physicsBody.destroy();
|
if (this.activeTriggers.has(record) && playerBody) {
|
||||||
|
this.onTriggerExit.Invoke({
|
||||||
|
lootableObject: record.lootableObject,
|
||||||
|
triggerObject: record.triggerObject,
|
||||||
|
playerBody,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.activeTriggers.delete(record);
|
this.activeTriggers.delete(record);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static update() {
|
private static update() {
|
||||||
const playerBody = Physics_internal.physicsWorld?.bodies.find(
|
const playerBody = this.playerBody;
|
||||||
(body) => body.collisionFilterGroup === PhysicsLayer.Player,
|
|
||||||
);
|
|
||||||
if (!playerBody) return;
|
if (!playerBody) return;
|
||||||
|
|
||||||
for (const trigger of this.triggers) {
|
for (const trigger of this.triggers) {
|
||||||
const dx = playerBody.position.x - trigger.position.x;
|
const dx = playerBody.position.x - trigger.center.x;
|
||||||
const dy = playerBody.position.y - trigger.position.y;
|
const dy = playerBody.position.y - trigger.center.y;
|
||||||
const dz = playerBody.position.z - trigger.position.z;
|
const dz = playerBody.position.z - trigger.center.z;
|
||||||
const distanceSq = dx * dx + dy * dy + dz * dz;
|
const distanceSq = dx * dx + dy * dy + dz * dz;
|
||||||
const isInside = distanceSq <= (trigger.radius + PLAYER_RADIUS) ** 2;
|
const isInside = distanceSq <= (trigger.radius + PLAYER_RADIUS) ** 2;
|
||||||
const wasInside = this.activeTriggers.has(trigger);
|
const wasInside = this.activeTriggers.has(trigger);
|
||||||
@@ -97,11 +107,9 @@ export class PhysicsTriggerC {
|
|||||||
if (isInside) {
|
if (isInside) {
|
||||||
this.activeTriggers.add(trigger);
|
this.activeTriggers.add(trigger);
|
||||||
this.onTriggerEnter.Invoke(payload);
|
this.onTriggerEnter.Invoke(payload);
|
||||||
console.log("[Gather Enter]", trigger.lootableObject.name);
|
|
||||||
} else {
|
} else {
|
||||||
this.activeTriggers.delete(trigger);
|
this.activeTriggers.delete(trigger);
|
||||||
this.onTriggerExit.Invoke(payload);
|
this.onTriggerExit.Invoke(payload);
|
||||||
console.log("[Gather Exit]", trigger.lootableObject.name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
import { Object3D } from "three";
|
import { EasyEvent, UpdateController } from "@24tools/playable_template";
|
||||||
|
import { Euler, Material, Mesh, Object3D, Vector3 } from "three";
|
||||||
import { PhysicsBody } from "../PhysicsC";
|
import { PhysicsBody } from "../PhysicsC";
|
||||||
import { PropType } from "./PropType";
|
import { PropType } from "./PropType";
|
||||||
import { PhysicsTriggerC } from "./PhysicsTriggerC";
|
import { PhysicsTriggerC } from "./PhysicsTriggerC";
|
||||||
import { PROP_DROPS } from "../Resources/PropDropTable";
|
import { PROP_DROPS } from "../Resources/PropDropTable";
|
||||||
import { createDropPlan, PropDropPlan } from "../Resources/PropDropPlanner";
|
import { createDropPlan, PropDropPlan } from "../Resources/PropDropPlanner";
|
||||||
import { ResourceSpawnC } from "../Resources/ResourceSpawnC";
|
import { ResourceSpawnC } from "../Resources/ResourceSpawnC";
|
||||||
|
import { PropHpUIC } from "./PropHpUIC";
|
||||||
|
import { PropHpBar } from "./PropHpBar";
|
||||||
|
import { PropVfxC } from "./PropVfxC";
|
||||||
|
import { VFXType } from "../Enums/VFXType";
|
||||||
|
|
||||||
|
/** Тривалість тряски після удару (секунди). */
|
||||||
|
const SHAKE_DURATION = 0.22;
|
||||||
|
/** Амплітуда зміщення моделі по X/Z під час тряски (world units). */
|
||||||
|
const SHAKE_POSITION_AMP = 0.045;
|
||||||
|
/** Амплітуда нахилу моделі під час тряски (радіани). */
|
||||||
|
const SHAKE_ROTATION_AMP = 0.1;
|
||||||
|
/** Частота коливань тряски (кількість «хитань» за секунду). */
|
||||||
|
const SHAKE_FREQUENCY = 2;
|
||||||
|
|
||||||
export class PropRegistry {
|
export class PropRegistry {
|
||||||
private static props = new Map<Object3D, PropC>();
|
private static props = new Map<Object3D, PropC>();
|
||||||
@@ -16,9 +30,15 @@ export class PropRegistry {
|
|||||||
static get(object: Object3D) {
|
static get(object: Object3D) {
|
||||||
return this.props.get(object);
|
return this.props.get(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static unregister(object: Object3D) {
|
||||||
|
this.props.delete(object);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PropC {
|
export class PropC {
|
||||||
|
static readonly onBroken = new EasyEvent<PropC>();
|
||||||
|
|
||||||
readonly object: Object3D;
|
readonly object: Object3D;
|
||||||
readonly propType: PropType;
|
readonly propType: PropType;
|
||||||
health: number;
|
health: number;
|
||||||
@@ -27,44 +47,171 @@ export class PropC {
|
|||||||
|
|
||||||
private wallBodies: PhysicsBody[] = [];
|
private wallBodies: PhysicsBody[] = [];
|
||||||
private dropPlan: PropDropPlan | null = null;
|
private dropPlan: PropDropPlan | null = null;
|
||||||
|
private hpBar: PropHpBar | null = null;
|
||||||
|
private damageLayers: Object3D[] = [];
|
||||||
|
|
||||||
constructor(object: Object3D, propType: PropType, maxHealth = 3) {
|
private readonly restPosition = new Vector3();
|
||||||
|
private readonly restRotation = new Euler();
|
||||||
|
private shakeTimeLeft = 0;
|
||||||
|
private shakeElapsed = 0;
|
||||||
|
private onShakeComplete: (() => void) | null = null;
|
||||||
|
|
||||||
|
private static shakingProps = new Set<PropC>();
|
||||||
|
private static shakeSystemInited = false;
|
||||||
|
|
||||||
|
constructor(object: Object3D, propType: PropType, maxHealth = 1) {
|
||||||
this.object = object;
|
this.object = object;
|
||||||
this.propType = propType;
|
this.propType = propType;
|
||||||
this.maxHealth = maxHealth;
|
this.maxHealth = maxHealth;
|
||||||
this.health = maxHealth;
|
this.health = maxHealth;
|
||||||
|
this.restPosition.copy(object.position);
|
||||||
|
this.restRotation.copy(object.rotation);
|
||||||
PropRegistry.register(this);
|
PropRegistry.register(this);
|
||||||
|
this.hpBar = PropHpUIC.createBar(this) ?? null;
|
||||||
|
PropC.ensureShakeSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ensureShakeSystem() {
|
||||||
|
if (this.shakeSystemInited) return;
|
||||||
|
this.shakeSystemInited = true;
|
||||||
|
|
||||||
|
UpdateController.Instance.onUpdate.addDelegate((delta) => {
|
||||||
|
for (const prop of [...this.shakingProps]) {
|
||||||
|
prop.updateShake(delta);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addWallBody(physicsBody: PhysicsBody) {
|
addWallBody(physicsBody: PhysicsBody) {
|
||||||
this.wallBodies.push(physicsBody);
|
this.wallBodies.push(physicsBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initDamageLayers(layers: Object3D[]) {
|
||||||
|
this.damageLayers = layers;
|
||||||
|
}
|
||||||
|
|
||||||
takeDamage(amount: number): boolean {
|
takeDamage(amount: number): boolean {
|
||||||
if (this.isBroken) return false;
|
if (this.isBroken) return false;
|
||||||
|
|
||||||
const hitIndex = this.maxHealth - this.health;
|
const hitIndex = this.maxHealth - this.health;
|
||||||
this.ensureDropPlan();
|
this.ensureDropPlan();
|
||||||
|
|
||||||
|
this.applyDamageVisual(hitIndex);
|
||||||
this.health -= amount;
|
this.health -= amount;
|
||||||
this.spawnForHit(hitIndex);
|
this.spawnForHit(hitIndex);
|
||||||
|
this.hpBar?.onDamage();
|
||||||
|
PropVfxC.Play(VFXType.LootableHit, null, this.object.getWorldPosition(new Vector3()));
|
||||||
|
|
||||||
if (this.health > 0) return false;
|
if (this.health > 0) {
|
||||||
|
this.playHitShake();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
this.break();
|
this.playHitShake(() => this.break());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyDamageVisual(hitIndex: number) {
|
||||||
|
const layer = this.damageLayers[hitIndex];
|
||||||
|
if (!layer) return;
|
||||||
|
|
||||||
|
layer.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private playHitShake(onComplete?: () => void) {
|
||||||
|
if (this.isBroken) return;
|
||||||
|
|
||||||
|
if (onComplete) {
|
||||||
|
this.onShakeComplete = onComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shakeTimeLeft = SHAKE_DURATION;
|
||||||
|
this.shakeElapsed = 0;
|
||||||
|
PropC.shakingProps.add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateShake(delta: number) {
|
||||||
|
if (this.shakeTimeLeft <= 0) {
|
||||||
|
this.finishShake();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shakeTimeLeft -= delta;
|
||||||
|
this.shakeElapsed += delta;
|
||||||
|
|
||||||
|
const progress = Math.max(this.shakeTimeLeft / SHAKE_DURATION, 0);
|
||||||
|
const intensity = progress * progress;
|
||||||
|
const phase = this.shakeElapsed * SHAKE_FREQUENCY * Math.PI * 2;
|
||||||
|
const wobble = Math.sin(phase);
|
||||||
|
|
||||||
|
this.object.position.set(
|
||||||
|
this.restPosition.x + wobble * SHAKE_POSITION_AMP * intensity,
|
||||||
|
this.restPosition.y,
|
||||||
|
this.restPosition.z + Math.cos(phase) * SHAKE_POSITION_AMP * intensity * 0.75,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.object.rotation.set(
|
||||||
|
this.restRotation.x + wobble * SHAKE_ROTATION_AMP * intensity * 0.35,
|
||||||
|
this.restRotation.y,
|
||||||
|
this.restRotation.z + Math.cos(phase) * SHAKE_ROTATION_AMP * intensity,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.shakeTimeLeft <= 0) {
|
||||||
|
this.finishShake();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private finishShake() {
|
||||||
|
this.stopShake();
|
||||||
|
|
||||||
|
const callback = this.onShakeComplete;
|
||||||
|
this.onShakeComplete = null;
|
||||||
|
callback?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopShake() {
|
||||||
|
this.object.position.copy(this.restPosition);
|
||||||
|
this.object.rotation.copy(this.restRotation);
|
||||||
|
this.shakeTimeLeft = 0;
|
||||||
|
PropC.shakingProps.delete(this);
|
||||||
|
}
|
||||||
|
|
||||||
break() {
|
break() {
|
||||||
if (this.isBroken) return;
|
if (this.isBroken) return;
|
||||||
|
|
||||||
this.isBroken = true;
|
this.isBroken = true;
|
||||||
this.object.visible = false;
|
this.onShakeComplete = null;
|
||||||
|
this.stopShake();
|
||||||
|
|
||||||
|
PropHpUIC.removeBar(this);
|
||||||
|
this.hpBar = null;
|
||||||
|
|
||||||
this.wallBodies.forEach((body) => body.destroy());
|
this.wallBodies.forEach((body) => body.destroy());
|
||||||
this.wallBodies = [];
|
this.wallBodies = [];
|
||||||
|
|
||||||
PhysicsTriggerC.unregister(this.object);
|
PhysicsTriggerC.unregister(this.object);
|
||||||
|
PropC.onBroken.Invoke(this);
|
||||||
|
PropRegistry.unregister(this.object);
|
||||||
|
|
||||||
|
PropVfxC.Play(VFXType.LootableDestroy, null, this.object.getWorldPosition(new Vector3()));
|
||||||
|
|
||||||
|
this.disposeProceduralResources();
|
||||||
|
this.object.parent?.remove(this.object);
|
||||||
|
}
|
||||||
|
|
||||||
|
private disposeProceduralResources() {
|
||||||
|
const trigger = this.object.getObjectByName("GatherTrigger");
|
||||||
|
if (!trigger || !(trigger as Mesh).isMesh) return;
|
||||||
|
|
||||||
|
const mesh = trigger as Mesh;
|
||||||
|
mesh.geometry.dispose();
|
||||||
|
|
||||||
|
const material = mesh.material;
|
||||||
|
if (Array.isArray(material)) {
|
||||||
|
material.forEach((entry) => entry.dispose());
|
||||||
|
} else if (material) {
|
||||||
|
(material as Material).dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureDropPlan() {
|
private ensureDropPlan() {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Object3D } from "three";
|
||||||
|
|
||||||
|
const STATE_LAYER_PATTERN = /_S(\d+)$/i;
|
||||||
|
|
||||||
|
function getStateLayerNumber(name: string): number | null {
|
||||||
|
const match = name.match(STATE_LAYER_PATTERN);
|
||||||
|
return match ? Number(match[1]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Повертає damage-шари з контейнера *_States*, відсортовані за номером _S*. */
|
||||||
|
export function findDamageStateLayers(root: Object3D): Object3D[] {
|
||||||
|
const containers: Object3D[] = [];
|
||||||
|
|
||||||
|
root.traverse((child) => {
|
||||||
|
if (/states/i.test(child.name)) {
|
||||||
|
containers.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const statesContainer = containers[0];
|
||||||
|
if (!statesContainer) return [];
|
||||||
|
|
||||||
|
return statesContainer.children
|
||||||
|
.filter((child) => getStateLayerNumber(child.name) !== null)
|
||||||
|
.sort((a, b) => getStateLayerNumber(a.name)! - getStateLayerNumber(b.name)!);
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { Box3, Mesh, Object3D, Vector3 } from "three";
|
||||||
|
import { CameraC } from "../CameraC";
|
||||||
|
import { PropC } from "./PropC";
|
||||||
|
|
||||||
|
const BAR_Y_OFFSET = 0.8;
|
||||||
|
const BAR_X_OFFSET = -0.3;
|
||||||
|
const CATCHUP_SPEED = 2.5;
|
||||||
|
|
||||||
|
export class PropHpBar {
|
||||||
|
private readonly prop: PropC;
|
||||||
|
private readonly root: HTMLElement;
|
||||||
|
private readonly currentEl: HTMLElement;
|
||||||
|
private readonly delayedEl: HTMLElement;
|
||||||
|
private readonly anchor = new Vector3();
|
||||||
|
private readonly bounds = new Box3();
|
||||||
|
|
||||||
|
private displayedHealth: number;
|
||||||
|
private visible = false;
|
||||||
|
|
||||||
|
constructor(prop: PropC, parent: HTMLElement) {
|
||||||
|
this.prop = prop;
|
||||||
|
this.displayedHealth = prop.health;
|
||||||
|
|
||||||
|
this.root = document.createElement("div");
|
||||||
|
this.root.className = "prop-hp-bar";
|
||||||
|
|
||||||
|
const track = document.createElement("div");
|
||||||
|
track.className = "prop-hp-bar__track";
|
||||||
|
|
||||||
|
this.delayedEl = document.createElement("div");
|
||||||
|
this.delayedEl.className = "prop-hp-bar__delayed";
|
||||||
|
|
||||||
|
this.currentEl = document.createElement("div");
|
||||||
|
this.currentEl.className = "prop-hp-bar__current";
|
||||||
|
|
||||||
|
track.appendChild(this.delayedEl);
|
||||||
|
track.appendChild(this.currentEl);
|
||||||
|
this.root.appendChild(track);
|
||||||
|
parent.appendChild(this.root);
|
||||||
|
|
||||||
|
this.applyWidths();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDamage() {
|
||||||
|
if (!this.visible) {
|
||||||
|
this.setVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.applyWidths();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta: number) {
|
||||||
|
if (this.prop.isBroken) return;
|
||||||
|
|
||||||
|
const { health, maxHealth } = this.prop;
|
||||||
|
if (this.displayedHealth > health) {
|
||||||
|
this.displayedHealth = Math.max(
|
||||||
|
health,
|
||||||
|
this.displayedHealth - CATCHUP_SPEED * delta * maxHealth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.delayedEl.style.width = `${this.getHealthRatio(this.displayedHealth) * 100}%`;
|
||||||
|
|
||||||
|
if (!this.visible) return;
|
||||||
|
|
||||||
|
this.updatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisible(value: boolean) {
|
||||||
|
this.visible = value;
|
||||||
|
this.root.classList.toggle("is-visible", value);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
this.displayedHealth = this.prop.health;
|
||||||
|
this.applyWidths();
|
||||||
|
this.updatePosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.root.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyWidths() {
|
||||||
|
const ratio = this.getHealthRatio(this.prop.health);
|
||||||
|
this.currentEl.style.width = `${ratio * 100}%`;
|
||||||
|
this.delayedEl.style.width = `${this.getHealthRatio(this.displayedHealth) * 100}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHealthRatio(health: number) {
|
||||||
|
if (this.prop.maxHealth <= 0) return 0;
|
||||||
|
return Math.max(health, 0) / this.prop.maxHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePosition() {
|
||||||
|
if (!this.computeVisualBounds()) {
|
||||||
|
this.root.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bounds.getCenter(this.anchor);
|
||||||
|
this.anchor.x += BAR_X_OFFSET;
|
||||||
|
this.anchor.y = this.bounds.max.y + BAR_Y_OFFSET;
|
||||||
|
|
||||||
|
const camera = CameraC.camera;
|
||||||
|
const projected = this.anchor.project(camera);
|
||||||
|
|
||||||
|
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() {
|
||||||
|
this.prop.object.updateWorldMatrix(true, true);
|
||||||
|
this.bounds.makeEmpty();
|
||||||
|
|
||||||
|
const includeMesh = (node: Object3D) => {
|
||||||
|
if (!(node as Mesh).isMesh) return;
|
||||||
|
|
||||||
|
const name = node.name.toLowerCase();
|
||||||
|
if (name.includes("collider") || name.includes("trigger") || name.includes("gathertrigger")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bounds.expandByObject(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
includeMesh(this.prop.object);
|
||||||
|
this.prop.object.traverse((child) => includeMesh(child));
|
||||||
|
|
||||||
|
if (!this.bounds.isEmpty()) return true;
|
||||||
|
|
||||||
|
this.bounds.setFromObject(this.prop.object);
|
||||||
|
return !this.bounds.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { UpdateController } from "@24tools/playable_template";
|
||||||
|
import { PropC } from "./PropC";
|
||||||
|
import { PropHpBar } from "./PropHpBar";
|
||||||
|
|
||||||
|
/** TODO: set false after HP bar position tuning */
|
||||||
|
const DEBUG_ALWAYS_VISIBLE_HP_BARS = false;
|
||||||
|
|
||||||
|
export class PropHpUIC {
|
||||||
|
private static inited = false;
|
||||||
|
private static root: HTMLElement | null = null;
|
||||||
|
private static bars = new Map<PropC, PropHpBar>();
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
if (this.inited) return;
|
||||||
|
this.inited = true;
|
||||||
|
|
||||||
|
const uiRoot = document.getElementById("ui");
|
||||||
|
if (!uiRoot) return;
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
for (const bar of this.bars.values()) {
|
||||||
|
bar.update(delta);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static createBar(prop: PropC): PropHpBar | undefined {
|
||||||
|
if (!this.root || this.bars.has(prop)) return this.bars.get(prop);
|
||||||
|
|
||||||
|
const bar = new PropHpBar(prop, this.root);
|
||||||
|
this.bars.set(prop, bar);
|
||||||
|
|
||||||
|
if (DEBUG_ALWAYS_VISIBLE_HP_BARS) {
|
||||||
|
bar.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
static showFor(props: PropC[]) {
|
||||||
|
if (DEBUG_ALWAYS_VISIBLE_HP_BARS) {
|
||||||
|
this.showAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const bar of this.bars.values()) {
|
||||||
|
bar.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const prop of props) {
|
||||||
|
if (prop.isBroken) continue;
|
||||||
|
this.bars.get(prop)?.setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static hideAll() {
|
||||||
|
if (DEBUG_ALWAYS_VISIBLE_HP_BARS) return;
|
||||||
|
|
||||||
|
for (const bar of this.bars.values()) {
|
||||||
|
bar.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static showAll() {
|
||||||
|
for (const [prop, bar] of this.bars) {
|
||||||
|
bar.setVisible(!prop.isBroken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeBar(prop: PropC) {
|
||||||
|
const bar = this.bars.get(prop);
|
||||||
|
if (!bar) return;
|
||||||
|
|
||||||
|
bar.destroy();
|
||||||
|
this.bars.delete(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { BatchedRenderer, QuarksLoader, QuarksUtil } from "three.quarks";
|
||||||
|
import { Object3D, Euler, Vector3 } from "three";
|
||||||
|
import { ResourcesC, UpdateController } from "@24tools/playable_template";
|
||||||
|
import { ThreeC } from "../ThreeC";
|
||||||
|
import { TimeC } from "../Timers/TimeC";
|
||||||
|
import { VFXType } from "../Enums/VFXType";
|
||||||
|
import { ResourcesType } from "../Presets/Enums/ResourcesType";
|
||||||
|
|
||||||
|
export class PropVfxC {
|
||||||
|
static batchRenderer: BatchedRenderer;
|
||||||
|
static loader: QuarksLoader;
|
||||||
|
|
||||||
|
static Init() {
|
||||||
|
this.batchRenderer = new BatchedRenderer();
|
||||||
|
this.loader = new QuarksLoader();
|
||||||
|
ThreeC.addToScene(this.batchRenderer);
|
||||||
|
UpdateController.Instance.onUpdate.addDelegate(this.update.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Remove(vfx: Object3D) {
|
||||||
|
vfx.removeFromParent();
|
||||||
|
vfx.parent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static update(delta: number) {
|
||||||
|
delta *= TimeC.TimeScale;
|
||||||
|
this.batchRenderer.update(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Play(
|
||||||
|
type: VFXType | string,
|
||||||
|
parent: Object3D | null = null,
|
||||||
|
position: Vector3 | null = null,
|
||||||
|
rotation: Euler | null = null,
|
||||||
|
scale: Vector3 | null = null,
|
||||||
|
odred: number | null = null,
|
||||||
|
) {
|
||||||
|
const loaded = (ResourcesC.getResource(ResourcesType.VFX, type.toString()) as { obj: Object3D } | undefined)?.obj;
|
||||||
|
if (!loaded) return new Object3D();
|
||||||
|
|
||||||
|
const effect = loaded.clone(true) as Object3D;
|
||||||
|
QuarksUtil.setAutoDestroy(effect, true);
|
||||||
|
QuarksUtil.addToBatchRenderer(effect, this.batchRenderer);
|
||||||
|
|
||||||
|
if (parent) parent.add(effect);
|
||||||
|
else ThreeC.addToScene(effect);
|
||||||
|
if (position) effect.position.copy(position);
|
||||||
|
if (rotation) effect.rotation.copy(rotation);
|
||||||
|
if (scale) effect.scale.copy(scale);
|
||||||
|
if (odred) effect.renderOrder = odred;
|
||||||
|
|
||||||
|
QuarksUtil.play(effect);
|
||||||
|
return effect;
|
||||||
|
}
|
||||||
|
|
||||||
|
static StopEmision(effect: Object3D) {
|
||||||
|
QuarksUtil.stop(effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Restart(effect: Object3D) {
|
||||||
|
QuarksUtil.play(effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Pause(effect: Object3D) {
|
||||||
|
QuarksUtil.pause(effect);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EasyEvent } from "@24tools/playable_template";
|
import { EasyEvent } from "@24tools/playable_template";
|
||||||
import { AnimationAction, AnimationClip, AnimationMixer, Color, LoopOnce, LoopRepeat, Mesh, MeshBasicMaterial, Object3D, Vector3 } from "three";
|
import { AnimationAction, AnimationClip, AnimationMixer, LoopOnce, LoopRepeat, Object3D, Vector3 } from "three";
|
||||||
import { clone } from "three/examples/jsm/utils/SkeletonUtils";
|
import { clone } from "three/examples/jsm/utils/SkeletonUtils";
|
||||||
import { ThreeC } from "../../ThreeC";
|
import { ThreeC } from "../../ThreeC";
|
||||||
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
|
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
|
||||||
@@ -9,23 +9,21 @@ export class Character {
|
|||||||
animMixer: AnimationMixer;
|
animMixer: AnimationMixer;
|
||||||
animationList: AnimationClip[] = [];
|
animationList: AnimationClip[] = [];
|
||||||
|
|
||||||
// isWalking: boolean = false;
|
|
||||||
curClipAction: null | AnimationAction = null;
|
curClipAction: null | AnimationAction = null;
|
||||||
animStopTimeout: null | NodeJS.Timeout = null
|
|
||||||
|
|
||||||
onAnimLoop: EasyEvent<{}> = new EasyEvent<{}>();
|
onAnimLoop: EasyEvent<{}> = new EasyEvent<{}>();
|
||||||
onAnimFinish: EasyEvent<{}> = new EasyEvent<{}>();
|
onAnimFinish: EasyEvent<{}> = new EasyEvent<{}>();
|
||||||
|
|
||||||
constructor(prefab: GLTF, start_position = new Vector3()) {
|
constructor(prefab: GLTF, start_position = new Vector3()) {
|
||||||
let tObj = clone(prefab.scene);
|
const tObj = clone(prefab.scene);
|
||||||
|
|
||||||
tObj.castShadow = true;
|
tObj.castShadow = true;
|
||||||
let animMixer = new AnimationMixer(tObj);
|
const animMixer = new AnimationMixer(tObj);
|
||||||
|
|
||||||
animMixer.addEventListener('loop', () => {
|
animMixer.addEventListener("loop", () => {
|
||||||
this.onAnimLoop.Invoke({});
|
this.onAnimLoop.Invoke({});
|
||||||
});
|
});
|
||||||
animMixer.addEventListener('finished', () => {
|
animMixer.addEventListener("finished", () => {
|
||||||
this.onAnimFinish.Invoke({});
|
this.onAnimFinish.Invoke({});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -35,21 +33,22 @@ export class Character {
|
|||||||
this.animMixer = animMixer;
|
this.animMixer = animMixer;
|
||||||
this.animationList = prefab.animations;
|
this.animationList = prefab.animations;
|
||||||
|
|
||||||
ThreeC.addToScene(tObj);
|
|
||||||
ThreeC.addAnimMixer(animMixer);
|
ThreeC.addAnimMixer(animMixer);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
set AnimationSpeed(timeScale: number) {
|
set AnimationSpeed(timeScale: number) {
|
||||||
if (this.curClipAction)
|
if (this.curClipAction) {
|
||||||
this.curClipAction.timeScale = timeScale;
|
this.curClipAction.timeScale = timeScale;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
set AnimationWeight(weight: number) {
|
set AnimationWeight(weight: number) {
|
||||||
if (this.curClipAction)
|
if (this.curClipAction) {
|
||||||
this.curClipAction.weight = weight;
|
this.curClipAction.weight = weight;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setObjectVisible(name: string, visible: boolean) {
|
setObjectVisible(name: string, visible: boolean) {
|
||||||
this.tObj.traverse((child) => {
|
this.tObj.traverse((child) => {
|
||||||
@@ -71,27 +70,63 @@ export class Character {
|
|||||||
this.setObjectVisible("Character_Pistol", false);
|
this.setObjectVisible("Character_Pistol", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
playAnimation(anim_id: number, one_time: boolean = false, fade = 0.25, randomStart = false) {
|
isPlayingAnimation(anim_id: number) {
|
||||||
let oldClipAction: null | AnimationAction = this.curClipAction;
|
if (!this.curClipAction || anim_id < 0 || anim_id >= this.animationList.length) {
|
||||||
var clipAction = this.animMixer.clipAction(this.animationList[anim_id]);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.curClipAction.getClip() === this.animationList[anim_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
playAnimation(anim_id: number, one_time = false, fade = 0.25, randomStart = false) {
|
||||||
|
const oldClipAction = this.curClipAction;
|
||||||
|
const clipAction = this.animMixer.clipAction(this.animationList[anim_id]);
|
||||||
|
|
||||||
if (one_time) {
|
if (one_time) {
|
||||||
clipAction.clampWhenFinished = true;
|
clipAction.clampWhenFinished = true;
|
||||||
clipAction.setLoop(LoopOnce, 1);
|
clipAction.setLoop(LoopOnce, 1);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
clipAction.clampWhenFinished = false;
|
clipAction.clampWhenFinished = false;
|
||||||
clipAction.setLoop(LoopRepeat, Infinity);
|
clipAction.setLoop(LoopRepeat, Infinity);
|
||||||
}
|
}
|
||||||
clipAction.timeScale = 1;
|
clipAction.timeScale = 1;
|
||||||
clipAction.weight = 1;
|
|
||||||
|
if (oldClipAction) {
|
||||||
|
oldClipAction.fadeOut(fade);
|
||||||
|
}
|
||||||
|
|
||||||
clipAction.reset();
|
clipAction.reset();
|
||||||
if (randomStart)
|
if (randomStart) {
|
||||||
clipAction.time = Math.random() * this.animationList[anim_id].duration;
|
clipAction.time = Math.random() * this.animationList[anim_id].duration;
|
||||||
|
}
|
||||||
clipAction.play();
|
clipAction.play();
|
||||||
if (oldClipAction && oldClipAction != clipAction)
|
clipAction.fadeIn(fade);
|
||||||
oldClipAction.crossFadeTo(clipAction, fade, true);
|
this.curClipAction = clipAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Blends from the current pose instead of snapping the new clip to frame 0. */
|
||||||
|
crossFadeToAnimation(anim_id: number, one_time = false, fade = 0.5) {
|
||||||
|
const oldClipAction = this.curClipAction;
|
||||||
|
const clipAction = this.animMixer.clipAction(this.animationList[anim_id]);
|
||||||
|
|
||||||
|
if (one_time) {
|
||||||
|
clipAction.clampWhenFinished = true;
|
||||||
|
clipAction.setLoop(LoopOnce, 1);
|
||||||
|
} else {
|
||||||
|
clipAction.clampWhenFinished = false;
|
||||||
|
clipAction.setLoop(LoopRepeat, Infinity);
|
||||||
|
}
|
||||||
|
clipAction.timeScale = 1;
|
||||||
|
|
||||||
|
if (oldClipAction && oldClipAction !== clipAction) {
|
||||||
|
clipAction.reset();
|
||||||
|
clipAction.play();
|
||||||
|
oldClipAction.crossFadeTo(clipAction, fade, false);
|
||||||
|
} else {
|
||||||
|
this.playAnimation(anim_id, one_time, fade);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.curClipAction = clipAction;
|
this.curClipAction = clipAction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export enum ResourcesType {
|
export enum ResourcesType {
|
||||||
Mesh = "mesh",
|
Mesh = "mesh",
|
||||||
|
VFX = "vfx",
|
||||||
}
|
}
|
||||||
@@ -2,5 +2,4 @@ import { Vector3 } from "three";
|
|||||||
|
|
||||||
export interface IMoveInput {
|
export interface IMoveInput {
|
||||||
get CurrentDirection(): Vector3;
|
get CurrentDirection(): Vector3;
|
||||||
update(delta);
|
|
||||||
}
|
}
|
||||||
@@ -1,109 +1,79 @@
|
|||||||
import { Delegate, JoystickC, UpdateController } from "@24tools/playable_template";
|
import { Delegate, JoystickC } from "@24tools/playable_template";
|
||||||
import { Vector3 } from "three";
|
import { Vector3 } from "three";
|
||||||
import { IMoveInput } from "./MoveInput";
|
import { IMoveInput } from "./MoveInput";
|
||||||
import { FollowCameraC } from "../Movment/CameraMovment/FollowCamera";
|
import { FollowCameraC } from "../Movment/CameraMovment/FollowCamera";
|
||||||
|
|
||||||
// type JoystickVectorData = {
|
|
||||||
// vector?: {
|
|
||||||
// x: number;
|
|
||||||
// y: number;
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// type JoystickPayload = {
|
|
||||||
// event?: Event;
|
|
||||||
// data?: JoystickVectorData;
|
|
||||||
// };
|
|
||||||
|
|
||||||
export class PlayerInput implements IMoveInput {
|
export class PlayerInput implements IMoveInput {
|
||||||
|
private static threshold = 0.25;
|
||||||
|
|
||||||
|
public static initJoystick() {
|
||||||
public static InitJoystick() {
|
|
||||||
const screenSize = window.screenSize;
|
const screenSize = window.screenSize;
|
||||||
const minSize = Math.min(screenSize.width, screenSize.height);
|
const minSize = Math.min(screenSize.width, screenSize.height);
|
||||||
const joystickSizeAspect = 0.2;
|
const joystickSizeAspect = 0.2;
|
||||||
const fadeTime = 200;
|
|
||||||
const options = {
|
const options = {
|
||||||
zone: document.getElementById("joystick_zone") as HTMLElement,
|
zone: document.getElementById("joystick_zone") as HTMLElement,
|
||||||
size: minSize * joystickSizeAspect,
|
size: minSize * joystickSizeAspect,
|
||||||
restJoystick: true,
|
restJoystick: true,
|
||||||
dynamicPage: true,
|
dynamicPage: true,
|
||||||
catchDistance: minSize * joystickSizeAspect / 2,
|
catchDistance: minSize * joystickSizeAspect / 2,
|
||||||
fadeTime: fadeTime,
|
fadeTime: 200,
|
||||||
};
|
};
|
||||||
JoystickC.init(options);
|
JoystickC.init(options);
|
||||||
|
|
||||||
// JoystickC.onJoysticMove.addDelegate(({ event, data }) => {
|
|
||||||
// console.log('onJoysticMove', event, data);
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected currentDirection: Vector3 = new Vector3();
|
protected currentDirection = new Vector3();
|
||||||
private updateDelegate: Delegate<number>;
|
private moveDelegate: Delegate<any>;
|
||||||
private StartDelegate: Delegate<any>;
|
private stopDelegate: Delegate<any>;
|
||||||
private MoveDelegate: Delegate<any>;
|
private startDelegate: Delegate<any>;
|
||||||
private StopDelegate: Delegate<any>;
|
|
||||||
private static threshold = 0.25;
|
|
||||||
|
|
||||||
get CurrentDirection() { return this.currentDirection.clone(); };
|
get CurrentDirection() {
|
||||||
get IsActive() { return this.inputaActive; };
|
return this.currentDirection;
|
||||||
|
}
|
||||||
|
|
||||||
inputaActive: boolean = false;
|
get IsActive() {
|
||||||
|
return this.inputActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputActive = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.updateDelegate = UpdateController.Instance.onUpdate.addDelegate(this.update.bind(this));
|
this.moveDelegate = JoystickC.onJoysticMove.addDelegate(this.onTouchMove.bind(this));
|
||||||
|
JoystickC.onJoysticDown.addListener(this.moveDelegate);
|
||||||
this.MoveDelegate = JoystickC.onJoysticMove.addDelegate(this.onTouchMove.bind(this));
|
this.stopDelegate = JoystickC.onJoysticEnd.addDelegate(this.onTouchUp.bind(this));
|
||||||
// "down" also carries joystick data in this SDK, useful for first non-zero direction.
|
this.startDelegate = JoystickC.onJoysticStart.addDelegate(this.onTouchDown.bind(this));
|
||||||
JoystickC.onJoysticDown.addListener(this.MoveDelegate);
|
|
||||||
|
|
||||||
this.StopDelegate = JoystickC.onJoysticEnd.addDelegate(this.onTouchUp.bind(this));
|
|
||||||
|
|
||||||
|
|
||||||
this.StartDelegate = JoystickC.onJoysticStart.addDelegate(this.onTouchDown.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
update(delta: number) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onTouchMove(payload: any) {
|
onTouchMove(payload: any) {
|
||||||
this.currentDirection = this.GetDiraction(payload);
|
this.getDirection(payload, this.currentDirection);
|
||||||
if (this.currentDirection.length() <= PlayerInput.threshold)
|
if (this.currentDirection.length() <= PlayerInput.threshold) {
|
||||||
this.currentDirection.multiplyScalar(0);
|
this.currentDirection.multiplyScalar(0);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onTouchDown(_event: any) {
|
onTouchDown(_event: any) {
|
||||||
// console.log(event);
|
if (this.inputActive) return;
|
||||||
|
this.inputActive = true;
|
||||||
if (this.inputaActive) return;
|
|
||||||
this.inputaActive = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onTouchUp() {
|
onTouchUp() {
|
||||||
if (!this.inputaActive) return;
|
if (!this.inputActive) return;
|
||||||
this.inputaActive = false;
|
this.inputActive = false;
|
||||||
this.currentDirection.multiplyScalar(0);
|
this.currentDirection.multiplyScalar(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
GetDiraction(payload: any) {
|
private getDirection(payload: any, out: Vector3) {
|
||||||
// Compatible with old/new nipplejs payloads wrapped by JoystickC:
|
|
||||||
// - { event, data } where data.vector exists
|
|
||||||
// - { event, data: undefined } where event.data.vector exists
|
|
||||||
const normalizedData =
|
const normalizedData =
|
||||||
payload?.data ??
|
payload?.data ??
|
||||||
payload?.event?.data ??
|
payload?.event?.data ??
|
||||||
payload?.event;
|
payload?.event;
|
||||||
const vector = normalizedData?.vector;
|
const vector = normalizedData?.vector;
|
||||||
if (!vector) return new Vector3();
|
if (!vector) {
|
||||||
|
out.set(0, 0, 0);
|
||||||
const x = vector.x;
|
return out;
|
||||||
const y = vector.y;
|
|
||||||
const dir = new Vector3(-x, 0, y);
|
|
||||||
dir.applyEuler(FollowCameraC.RotationCorection);
|
|
||||||
|
|
||||||
return dir;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out.set(-vector.x, 0, vector.y);
|
||||||
|
out.applyEuler(FollowCameraC.RotationCorrection);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,23 +12,21 @@ export class FollowCameraC {
|
|||||||
static cameraContainer: Object3D = new Object3D();
|
static cameraContainer: Object3D = new Object3D();
|
||||||
static cameraRotation: Object3D = new Object3D();
|
static cameraRotation: Object3D = new Object3D();
|
||||||
|
|
||||||
/** Normalized movement direction set each frame by the player */
|
|
||||||
static inputDirection: Vector3 = new Vector3();
|
static inputDirection: Vector3 = new Vector3();
|
||||||
/** How far (world units) the camera shifts ahead of the player */
|
static lookAheadAmount = 1;
|
||||||
static lookAheadAmount: number = 2;
|
static lookAheadLerpSpeed = 0.7;
|
||||||
/** Lerp speed for the look-ahead offset (lower = smoother/slower) */
|
|
||||||
static lookAheadLerpSpeed: number = 0.7;
|
|
||||||
|
|
||||||
private static lookAheadCurrent: Vector3 = new Vector3();
|
private static lookAheadCurrent: Vector3 = new Vector3();
|
||||||
|
private static lookAheadTarget: Vector3 = new Vector3();
|
||||||
|
private static basePos: Vector3 = new Vector3();
|
||||||
|
private static targetPos: Vector3 = new Vector3();
|
||||||
|
private static oldPos: Vector3 = new Vector3();
|
||||||
|
|
||||||
static Init(target: Object3D) {
|
static init(target: Object3D) {
|
||||||
this.target = target;
|
this.target = target;
|
||||||
this.offset = this.Offset;
|
this.offset = this.Offset;
|
||||||
|
|
||||||
console.log(target);
|
this.updateDelegate = new Delegate<number>(this.update.bind(this));
|
||||||
|
|
||||||
|
|
||||||
this.updateDelegate = new Delegate<number>(this.Update.bind(this));
|
|
||||||
UpdateController.Instance.onUpdate.addListener(this.updateDelegate);
|
UpdateController.Instance.onUpdate.addListener(this.updateDelegate);
|
||||||
|
|
||||||
ThreeC.addToScene(this.mainContainer);
|
ThreeC.addToScene(this.mainContainer);
|
||||||
@@ -45,49 +43,41 @@ export class FollowCameraC {
|
|||||||
this.cameraContainer.position.y += this.Offset.y;
|
this.cameraContainer.position.y += this.Offset.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Update(delta: number) {
|
private static update(delta: number) {
|
||||||
if (!this.target.position) return;
|
if (!this.target.position) return;
|
||||||
|
|
||||||
// Lerp look-ahead toward current movement direction (stays at last direction when stopped)
|
this.lookAheadTarget.copy(this.inputDirection).multiplyScalar(this.lookAheadAmount);
|
||||||
const lookAheadTarget = this.inputDirection.clone().multiplyScalar(this.lookAheadAmount);
|
this.lookAheadCurrent.lerp(this.lookAheadTarget, delta * this.lookAheadLerpSpeed);
|
||||||
this.lookAheadCurrent.lerp(lookAheadTarget, delta * this.lookAheadLerpSpeed);
|
|
||||||
|
|
||||||
const offset = this.Offset;
|
const offset = this.Offset;
|
||||||
|
|
||||||
// Base position without look-ahead
|
this.basePos.copy(this.target.position);
|
||||||
const basePos = this.target.position.clone();
|
this.basePos.x += offset.x;
|
||||||
basePos.x += offset.x;
|
this.basePos.z += offset.z;
|
||||||
basePos.z += offset.z;
|
|
||||||
|
|
||||||
// Final target = base + look-ahead shift
|
this.targetPos.copy(this.basePos);
|
||||||
const targetPos = basePos.clone();
|
this.targetPos.x += this.lookAheadCurrent.x;
|
||||||
targetPos.x += this.lookAheadCurrent.x;
|
this.targetPos.z += this.lookAheadCurrent.z;
|
||||||
targetPos.z += this.lookAheadCurrent.z;
|
|
||||||
|
|
||||||
// Capture actual previous position BEFORE any mutation
|
this.oldPos.copy(this.mainContainer.position);
|
||||||
const oldPos = this.mainContainer.position.clone();
|
|
||||||
|
|
||||||
// Compute rotation using base position to avoid tilt from look-ahead offset
|
this.mainContainer.position.copy(this.basePos);
|
||||||
this.mainContainer.position.copy(basePos);
|
|
||||||
this.mainContainer.lookAt(this.target.position);
|
this.mainContainer.lookAt(this.target.position);
|
||||||
this.cameraContainer.lookAt(this.target.position);
|
this.cameraContainer.lookAt(this.target.position);
|
||||||
|
|
||||||
// Smooth follow lerp from real previous position toward target with look-ahead
|
|
||||||
const lerpSpeed = 10;
|
const lerpSpeed = 10;
|
||||||
this.mainContainer.position.lerpVectors(oldPos, targetPos, delta * lerpSpeed);
|
this.mainContainer.position.lerpVectors(this.oldPos, this.targetPos, delta * lerpSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get RotationCorection() {
|
static get RotationCorrection() {
|
||||||
const rotation = this.mainContainer.rotation.clone();
|
return this.mainContainer.rotation.clone();
|
||||||
return rotation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get Offset() {
|
static get Offset() {
|
||||||
const portrait = window.screenSize.portrait
|
const portrait = window.screenSize.portrait;
|
||||||
const values = portrait
|
const values = portrait
|
||||||
? Template.getValue<number[]>("global", "camera_position_p")
|
? Template.getValue<number[]>("global", "camera_position_p")
|
||||||
: Template.getValue<number[]>("global", "camera_position_l");
|
: Template.getValue<number[]>("global", "camera_position_l");
|
||||||
const offset = Helper.returnVectorCamera(values);
|
return Helper.returnVectorCamera(values);
|
||||||
return offset
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
import { Delegate, UpdateController } from "@24tools/playable_template";
|
|
||||||
import { IMoveInput } from "../Input/MoveInput";
|
import { IMoveInput } from "../Input/MoveInput";
|
||||||
import { Vector3 } from "three";
|
import { Vector3 } from "three";
|
||||||
|
|
||||||
export class MoveC {
|
export class MoveC {
|
||||||
private input: IMoveInput;
|
private readonly input: IMoveInput;
|
||||||
private speed: number = 5;
|
private readonly speed: number;
|
||||||
private updateDelegate: Delegate<number>;
|
private readonly moveDirection = new Vector3();
|
||||||
private moveDiraction: Vector3 = new Vector3();
|
|
||||||
get Diraction() { return this.moveDiraction };
|
|
||||||
get Weight() { return this.moveDiraction.length() / this.speed };
|
|
||||||
|
|
||||||
constructor(Input: IMoveInput, speed: number = 5) {
|
get Direction() {
|
||||||
this.updateDelegate = new Delegate<number>(this.update.bind(this));
|
return this.moveDirection;
|
||||||
UpdateController.Instance.onUpdate.addListener(this.updateDelegate);
|
}
|
||||||
this.input = Input;
|
|
||||||
|
get Weight() {
|
||||||
|
return this.moveDirection.length() / this.speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(input: IMoveInput, speed = 5) {
|
||||||
|
this.input = input;
|
||||||
this.speed = speed;
|
this.speed = speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private update(delta: number) {
|
update(_delta: number) {
|
||||||
// delta *= TimeC.TimeScale;
|
this.moveDirection.copy(this.input.CurrentDirection).multiplyScalar(this.speed);
|
||||||
const moveStep = this.input.CurrentDirection.multiplyScalar(this.speed);
|
|
||||||
this.moveDiraction.copy(moveStep);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,37 +1,76 @@
|
|||||||
import { Delegate, UpdateController } from "@24tools/playable_template";
|
|
||||||
import { IMoveInput } from "../Input/MoveInput";
|
|
||||||
import { Object3D, Quaternion, Vector3 } from "three";
|
import { Object3D, Quaternion, Vector3 } from "three";
|
||||||
|
|
||||||
export class RotationC {
|
export class RotationC {
|
||||||
private target: Object3D;
|
private static readonly completeAngle = 0.01;
|
||||||
private input: IMoveInput;
|
|
||||||
private speed: number = 5;
|
|
||||||
private updateDelegate: Delegate<number>;
|
|
||||||
private currentQ: Quaternion = new Quaternion();
|
|
||||||
private targetQ: Quaternion = new Quaternion();
|
|
||||||
|
|
||||||
constructor(target: Object3D, Input: IMoveInput, speed: number = 5) {
|
private readonly target: Object3D;
|
||||||
this.updateDelegate = new Delegate<number>(this.update.bind(this));
|
private readonly speed: number;
|
||||||
UpdateController.Instance.onUpdate.addListener(this.updateDelegate);
|
private readonly targetDirection = new Vector3(0, 0, 1);
|
||||||
this.input = Input;
|
private readonly worldPosition = new Vector3();
|
||||||
this.speed = speed;
|
private readonly currentQ = new Quaternion();
|
||||||
|
private readonly targetQ = new Quaternion();
|
||||||
|
private readonly lookAtPoint = new Vector3();
|
||||||
|
|
||||||
|
constructor(target: Object3D, speed: number = 5) {
|
||||||
this.target = target;
|
this.target = target;
|
||||||
this.currentQ.copy(this.target.quaternion);
|
this.speed = speed;
|
||||||
this.targetQ.copy(this.target.quaternion);
|
this.currentQ.copy(target.quaternion);
|
||||||
|
this.targetQ.copy(target.quaternion);
|
||||||
|
target.getWorldDirection(this.targetDirection);
|
||||||
|
this.targetDirection.y = 0;
|
||||||
|
if (this.targetDirection.lengthSq() > 0) {
|
||||||
|
this.targetDirection.normalize();
|
||||||
|
} else {
|
||||||
|
this.targetDirection.set(0, 0, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private update(delta: number) {
|
setTargetDirection(direction: Vector3) {
|
||||||
const loockAtStep = this.input.CurrentDirection;
|
if (direction.lengthSq() === 0) return;
|
||||||
if (loockAtStep.length() == 0) return;
|
this.targetDirection.copy(direction).normalize();
|
||||||
|
}
|
||||||
|
|
||||||
this.currentQ.copy(this.target.quaternion);
|
setTargetWorldPosition(worldPosition: Vector3) {
|
||||||
|
this.target.getWorldPosition(this.worldPosition);
|
||||||
|
this.targetDirection.set(
|
||||||
|
worldPosition.x - this.worldPosition.x,
|
||||||
|
0,
|
||||||
|
worldPosition.z - this.worldPosition.z,
|
||||||
|
);
|
||||||
|
if (this.targetDirection.lengthSq() === 0) return;
|
||||||
|
this.targetDirection.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
const loockAtPoint = this.target.position.clone().add(loockAtStep);
|
syncToCurrentFacing() {
|
||||||
this.target.lookAt(loockAtPoint);
|
this.target.getWorldDirection(this.targetDirection);
|
||||||
this.targetQ.copy(this.target.quaternion);
|
this.targetDirection.y = 0;
|
||||||
this.target.quaternion.copy(this.currentQ);
|
if (this.targetDirection.lengthSq() === 0) return;
|
||||||
|
this.targetDirection.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
isComplete() {
|
||||||
|
this.computeTargetQuaternion(this.targetQ);
|
||||||
|
return this.currentQ.angleTo(this.targetQ) < RotationC.completeAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta: number) {
|
||||||
|
this.computeTargetQuaternion(this.targetQ);
|
||||||
|
|
||||||
|
if (this.currentQ.angleTo(this.targetQ) < RotationC.completeAngle) {
|
||||||
|
this.target.quaternion.copy(this.targetQ);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.target.quaternion.slerp(this.targetQ, delta * this.speed);
|
this.target.quaternion.slerp(this.targetQ, delta * this.speed);
|
||||||
this.currentQ.copy(this.target.quaternion);
|
this.currentQ.copy(this.target.quaternion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private computeTargetQuaternion(out: Quaternion) {
|
||||||
|
this.currentQ.copy(this.target.quaternion);
|
||||||
|
|
||||||
|
this.lookAtPoint.copy(this.target.position).add(this.targetDirection);
|
||||||
|
this.target.lookAt(this.lookAtPoint);
|
||||||
|
out.copy(this.target.quaternion);
|
||||||
|
this.target.quaternion.copy(this.currentQ);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,47 +5,36 @@ import { MeshType } from "./Enums/MeshType";
|
|||||||
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
|
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
|
||||||
import { BaseAnimation } from "./Enums/BaseAnimation";
|
import { BaseAnimation } from "./Enums/BaseAnimation";
|
||||||
import { PhysicsBody, PhysicsLayer } from "../PhysicsC";
|
import { PhysicsBody, PhysicsLayer } from "../PhysicsC";
|
||||||
import { Object3D, Quaternion, Vector3 } from "three";
|
import { Object3D, Vector3 } from "three";
|
||||||
import { ThreeC } from "../ThreeC";
|
import { ThreeC } from "../ThreeC";
|
||||||
import { PlayerInput } from "./Input/PlayerInput";
|
import { PlayerInput } from "./Input/PlayerInput";
|
||||||
import { MoveC } from "./Movment/MoveC";
|
import { MoveC } from "./Movment/MoveC";
|
||||||
import { Vector3CToT, Vector3TToC } from "./Helper";
|
import { Vector3CToT, Vector3TToC } from "./Helper";
|
||||||
import { RotationC } from "./Movment/RotationC";
|
import { RotationC } from "./Movment/RotationC";
|
||||||
import { FollowCameraC } from "./Movment/CameraMovment/FollowCamera";
|
import { FollowCameraC } from "./Movment/CameraMovment/FollowCamera";
|
||||||
|
import { PlayerCombat } from "./PlayerCombat";
|
||||||
|
import { PhysicsTriggerC } from "../Map/PhysicsTriggerC";
|
||||||
|
import { PropHpUIC } from "../Map/PropHpUIC";
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
private static inited: boolean = false;
|
private static inited = false;
|
||||||
private static isRunning: boolean = false;
|
private static isRunning = false;
|
||||||
private static isAttacking: boolean = false;
|
|
||||||
private static isAutoAttacking: boolean = false;
|
|
||||||
private static isTurningToTarget: boolean = false;
|
|
||||||
private static isBatEquipped: boolean = false;
|
|
||||||
private static attackPhase: "none" | "attacking" = "none";
|
|
||||||
|
|
||||||
private static updateDelegate: Delegate<number>;
|
private static updateDelegate: Delegate<number>;
|
||||||
private static onAttackComplete: (() => void) | null = null;
|
|
||||||
private static onTurnComplete: (() => void) | null = null;
|
|
||||||
private static turnTargetQ = new Quaternion();
|
|
||||||
|
|
||||||
private static container: Object3D = new Object3D;
|
private static container: Object3D = new Object3D();
|
||||||
private static input: PlayerInput;
|
private static input: PlayerInput;
|
||||||
private static movement: MoveC;
|
private static movement: MoveC;
|
||||||
private static rotation: RotationC;
|
private static rotation: RotationC;
|
||||||
private static spawnPosition: Vector3 = new Vector3(0, 0, 0);
|
private static spawnPosition = new Vector3(0, 0, -4);
|
||||||
|
|
||||||
private static readonly turnSpeed = 8;
|
|
||||||
private static readonly turnCompleteAngle = 0.05;
|
|
||||||
|
|
||||||
static character: Character;
|
static character: Character;
|
||||||
static physics: PhysicsBody;
|
static physics: PhysicsBody;
|
||||||
|
|
||||||
static SetSpawnPosition(position: Vector3) {
|
static init() {
|
||||||
this.spawnPosition.copy(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Init() {
|
|
||||||
if (this.inited) return;
|
if (this.inited) return;
|
||||||
this.inited = true;
|
this.inited = true;
|
||||||
|
|
||||||
const asset = ResourcesC.getResource<GLTF>(ResourcesType.Mesh, MeshType.Character);
|
const asset = ResourcesC.getResource<GLTF>(ResourcesType.Mesh, MeshType.Character);
|
||||||
this.character = new Character(asset);
|
this.character = new Character(asset);
|
||||||
|
|
||||||
@@ -55,19 +44,31 @@ export class Player {
|
|||||||
this.container.add(this.character.tObj);
|
this.container.add(this.character.tObj);
|
||||||
ThreeC.addToScene(this.container);
|
ThreeC.addToScene(this.container);
|
||||||
|
|
||||||
this.InitPhisic();
|
this.initPhysics();
|
||||||
|
PhysicsTriggerC.setPlayerBody(this.physics.getPhysicsBody());
|
||||||
|
|
||||||
this.input = new PlayerInput();
|
this.input = new PlayerInput();
|
||||||
const moveSpeed = 3;
|
const moveSpeed = 3;
|
||||||
const rotationSpeed = 8;
|
const rotationSpeed = 8;
|
||||||
this.movement = new MoveC(this.input, moveSpeed);
|
this.movement = new MoveC(this.input, moveSpeed);
|
||||||
this.rotation = new RotationC(this.container, this.input, rotationSpeed);
|
this.rotation = new RotationC(this.container, rotationSpeed);
|
||||||
|
|
||||||
this.character.onAnimFinish.addDelegate(() => this.OnCombatAnimationFinished());
|
PlayerCombat.init({
|
||||||
|
character: this.character,
|
||||||
|
getPhysicsBody: () => this.physics.getPhysicsBody(),
|
||||||
|
isMoving: () => this.isMoving(),
|
||||||
|
onStopRunning: () => this.stopRunning(),
|
||||||
|
onResumeRunning: () => this.resumeRunning(),
|
||||||
|
onSetTurnTarget: (worldPosition) => this.rotation.setTargetWorldPosition(worldPosition),
|
||||||
|
onUpdateRotation: (delta) => this.rotation.update(delta),
|
||||||
|
isRotationComplete: () => this.rotation.isComplete(),
|
||||||
|
onSyncRotation: () => this.syncRotationToFacing(),
|
||||||
|
});
|
||||||
|
|
||||||
this.updateDelegate = new Delegate<number>((delta) => this.Update(delta));
|
this.updateDelegate = new Delegate<number>((delta) => this.update(delta));
|
||||||
UpdateController.Instance.onUpdate.addListener(this.updateDelegate);
|
UpdateController.Instance.onUpdate.addListener(this.updateDelegate);
|
||||||
|
|
||||||
FollowCameraC.Init(this.container);
|
FollowCameraC.init(this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getWorldPosition() {
|
static getWorldPosition() {
|
||||||
@@ -75,42 +76,39 @@ export class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static isMoving() {
|
static isMoving() {
|
||||||
return this.input.IsActive;
|
return this.movement.Direction.lengthSq() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static startAutoAttack(onHit: () => void, targetWorldPosition: Vector3) {
|
static isCombatBusy() {
|
||||||
if (this.isAutoAttacking) return;
|
return PlayerCombat.isBusy;
|
||||||
|
}
|
||||||
|
|
||||||
this.isAutoAttacking = true;
|
static isAutoAttackActive() {
|
||||||
this.beginTurnToTarget(targetWorldPosition, () => this.playAttack(onHit));
|
return PlayerCombat.isAutoAttackActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
static startAutoAttack(
|
||||||
|
onStrike: () => void,
|
||||||
|
onComplete: () => void,
|
||||||
|
targetWorldPosition: Vector3,
|
||||||
|
) {
|
||||||
|
PlayerCombat.startAutoAttack(onStrike, onComplete, targetWorldPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
static stopAutoAttack() {
|
static stopAutoAttack() {
|
||||||
this.isAutoAttacking = false;
|
PlayerCombat.stopAutoAttack();
|
||||||
this.onAttackComplete = null;
|
PropHpUIC.hideAll();
|
||||||
this.cancelTurnToTarget();
|
|
||||||
this.resetCombatState();
|
|
||||||
|
|
||||||
if (!this.isAttacking) {
|
|
||||||
this.StopRunning();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static playAttack(onComplete: () => void) {
|
static retargetAutoAttack(targetWorldPosition: Vector3) {
|
||||||
this.onAttackComplete = onComplete;
|
PlayerCombat.retargetAutoAttack(targetWorldPosition);
|
||||||
this.isAttacking = true;
|
|
||||||
this.isRunning = false;
|
|
||||||
|
|
||||||
this.physics.getPhysicsBody().velocity.set(0, 0, 0);
|
|
||||||
|
|
||||||
if (!this.isBatEquipped) {
|
|
||||||
this.equipBat();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playBatAttack();
|
static playAttack(onStrike: () => void) {
|
||||||
|
PlayerCombat.playAttack(onStrike);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InitPhisic() {
|
private static initPhysics() {
|
||||||
this.physics = new PhysicsBody(
|
this.physics = new PhysicsBody(
|
||||||
this.container,
|
this.container,
|
||||||
false,
|
false,
|
||||||
@@ -120,136 +118,77 @@ export class Player {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static beginTurnToTarget(targetWorldPosition: Vector3, onComplete: () => void) {
|
private static startRunning() {
|
||||||
const worldPosition = this.container.getWorldPosition(new Vector3());
|
if (PlayerCombat.isBusy) return;
|
||||||
const lookAtPoint = targetWorldPosition.clone();
|
if (this.isRunning) return;
|
||||||
lookAtPoint.y = worldPosition.y;
|
if (this.character.isPlayingAnimation(BaseAnimation.Run)) {
|
||||||
|
this.isRunning = true;
|
||||||
const turnHelper = new Object3D();
|
|
||||||
turnHelper.position.copy(worldPosition);
|
|
||||||
turnHelper.lookAt(lookAtPoint);
|
|
||||||
this.turnTargetQ.copy(turnHelper.quaternion);
|
|
||||||
|
|
||||||
this.onTurnComplete = onComplete;
|
|
||||||
this.isTurningToTarget = true;
|
|
||||||
this.physics.getPhysicsBody().velocity.set(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static cancelTurnToTarget() {
|
|
||||||
this.isTurningToTarget = false;
|
|
||||||
this.onTurnComplete = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static updateTurnToTarget(delta: number) {
|
|
||||||
this.container.quaternion.slerp(this.turnTargetQ, delta * this.turnSpeed);
|
|
||||||
|
|
||||||
if (this.container.quaternion.angleTo(this.turnTargetQ) > this.turnCompleteAngle) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.container.quaternion.copy(this.turnTargetQ);
|
|
||||||
this.isTurningToTarget = false;
|
|
||||||
|
|
||||||
const callback = this.onTurnComplete;
|
|
||||||
this.onTurnComplete = null;
|
|
||||||
callback?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static playBatAttack() {
|
|
||||||
this.attackPhase = "attacking";
|
|
||||||
this.character.playAnimation(this.getBatAttackAnimation(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static getBatAttackAnimation() {
|
|
||||||
if (this.character.animationList.length > BaseAnimation.Loot) {
|
|
||||||
return BaseAnimation.Loot;
|
|
||||||
}
|
|
||||||
|
|
||||||
return BaseAnimation.Idle;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static equipBat() {
|
|
||||||
this.isBatEquipped = true;
|
|
||||||
this.character.setBatEquipped(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static resetCombatState() {
|
|
||||||
this.attackPhase = "none";
|
|
||||||
this.isAttacking = false;
|
|
||||||
this.isBatEquipped = false;
|
|
||||||
this.character.setDefaultWeapons();
|
|
||||||
this.character.playAnimation(BaseAnimation.Idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static OnCombatAnimationFinished() {
|
|
||||||
if (this.attackPhase !== "attacking") return;
|
|
||||||
|
|
||||||
this.attackPhase = "none";
|
|
||||||
this.isAttacking = false;
|
|
||||||
|
|
||||||
const callback = this.onAttackComplete;
|
|
||||||
this.onAttackComplete = null;
|
|
||||||
callback?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static StartRunning() {
|
|
||||||
if (this.isRunning || this.isAttacking || this.isTurningToTarget) return;
|
|
||||||
this.character.playAnimation(BaseAnimation.Run);
|
this.character.playAnimation(BaseAnimation.Run);
|
||||||
this.isRunning = true;
|
this.isRunning = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static StopRunning() {
|
private static resumeRunning() {
|
||||||
if (!this.isRunning || this.isAttacking || this.isTurningToTarget) return;
|
if (PlayerCombat.isBusy) return;
|
||||||
|
this.isRunning = true;
|
||||||
|
if (this.character.isPlayingAnimation(BaseAnimation.Run)) return;
|
||||||
|
this.character.crossFadeToAnimation(BaseAnimation.Run, false, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static stopRunning() {
|
||||||
|
if (!this.isRunning) return;
|
||||||
|
if (!PlayerCombat.isBusy) {
|
||||||
this.character.playAnimation(BaseAnimation.Idle);
|
this.character.playAnimation(BaseAnimation.Idle);
|
||||||
this.AnimationValue = 1;
|
}
|
||||||
|
this.animationValue = 1;
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static set AnimationValue(value: number) {
|
private static set animationValue(value: number) {
|
||||||
this.character.AnimationSpeed = value;
|
this.character.AnimationSpeed = value;
|
||||||
this.character.AnimationWeight = value * 12.5 + 87.5;
|
this.character.AnimationWeight = value * 12.5 + 87.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Update(delta: number) {
|
private static update(delta: number) {
|
||||||
if (this.input.IsActive && (this.isAutoAttacking || this.isTurningToTarget)) {
|
this.movement.update(delta);
|
||||||
this.stopAutoAttack();
|
|
||||||
|
if (this.isMoving()) {
|
||||||
|
PlayerCombat.cancelOnMoveInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isTurningToTarget) {
|
if (PlayerCombat.update(delta)) {
|
||||||
this.physics.getPhysicsBody().velocity.set(0, 0, 0);
|
this.syncVisual(delta);
|
||||||
this.updateTurnToTarget(delta);
|
|
||||||
this.MoveVisual(delta);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isAttacking) {
|
const direction = this.movement.Direction;
|
||||||
this.physics.getPhysicsBody().velocity.set(0, 0, 0);
|
|
||||||
this.MoveVisual(delta);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const diraction = this.movement.Diraction;
|
|
||||||
const weight = this.movement.Weight;
|
const weight = this.movement.Weight;
|
||||||
|
const isMoving = direction.lengthSq() > 0;
|
||||||
|
|
||||||
if (diraction.length() > 0) {
|
if (isMoving) {
|
||||||
this.StartRunning();
|
this.startRunning();
|
||||||
this.AnimationValue = weight;
|
this.animationValue = weight;
|
||||||
FollowCameraC.inputDirection.copy(diraction).normalize();
|
FollowCameraC.inputDirection.copy(direction).normalize();
|
||||||
|
this.rotation.setTargetDirection(direction);
|
||||||
} else {
|
} else {
|
||||||
this.StopRunning();
|
this.stopRunning();
|
||||||
}
|
}
|
||||||
|
|
||||||
const cPos = Vector3TToC(diraction);
|
this.rotation.update(delta);
|
||||||
|
|
||||||
this.physics.getPhysicsBody().velocity.copy(cPos);
|
this.physics.getPhysicsBody().velocity.copy(Vector3TToC(direction));
|
||||||
this.physics.getPhysicsBody().wakeUp();
|
this.physics.getPhysicsBody().wakeUp();
|
||||||
this.MoveVisual(delta);
|
this.syncVisual(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MoveVisual(delta: number) {
|
private static syncRotationToFacing() {
|
||||||
|
this.rotation.syncToCurrentFacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static syncVisual(delta: number) {
|
||||||
const lerpSpeed = 10;
|
const lerpSpeed = 10;
|
||||||
const targetPos = Vector3CToT(this.physics.getPhysicsBody().position);
|
const targetPos = Vector3CToT(this.physics.getPhysicsBody().position);
|
||||||
|
|
||||||
this.container.position.lerp(targetPos, delta * lerpSpeed);
|
this.container.position.lerp(targetPos, delta * lerpSpeed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
import { Vector3 } from "three";
|
||||||
|
import { Body } from "cannon-es";
|
||||||
|
import { Character } from "./Character/Character";
|
||||||
|
import { BaseAnimation } from "./Enums/BaseAnimation";
|
||||||
|
|
||||||
|
type PlayerCombatDeps = {
|
||||||
|
character: Character;
|
||||||
|
getPhysicsBody: () => Body;
|
||||||
|
isMoving: () => boolean;
|
||||||
|
onStopRunning: () => void;
|
||||||
|
onResumeRunning: () => void;
|
||||||
|
onSetTurnTarget: (worldPosition: Vector3) => void;
|
||||||
|
onUpdateRotation: (delta: number) => void;
|
||||||
|
isRotationComplete: () => boolean;
|
||||||
|
onSyncRotation: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PlayerCombat {
|
||||||
|
private static deps: PlayerCombatDeps;
|
||||||
|
|
||||||
|
private static isAttacking = false;
|
||||||
|
private static isAutoAttacking = false;
|
||||||
|
private static isTurningToTarget = false;
|
||||||
|
private static isBatEquipped = false;
|
||||||
|
private static isLootLoopActive = false;
|
||||||
|
private static pendingCombatExit = false;
|
||||||
|
private static attackPhase: "none" | "attacking" = "none";
|
||||||
|
|
||||||
|
private static onStrike: (() => void) | null = null;
|
||||||
|
private static onTurnComplete: (() => void) | null = null;
|
||||||
|
private static strikeMarkIndex = 0;
|
||||||
|
private static prevStrikeNormTime = 0;
|
||||||
|
|
||||||
|
private static readonly turnIdleFade = 0.1;
|
||||||
|
private static readonly lootEnterFade = 0.25;
|
||||||
|
private static readonly combatExitFade = 0.8;
|
||||||
|
/** Normalized clip time (0–1) when the bat crosses the target: R→L, then L→R. */
|
||||||
|
private static readonly attackStrikeMarks = [0.38, 0.72];
|
||||||
|
/** After 1st hit (R→L) — safe exit if combat ends before 2nd hit (L→R). */
|
||||||
|
private static readonly lootExitAfterFirstStrike = 0.5;
|
||||||
|
/** After 2nd hit (L→R) — follow-through before idle. */
|
||||||
|
private static readonly lootExitAfterSecondStrike = 0.78;
|
||||||
|
/** End of clip when exiting before any strike (e.g. left the zone). */
|
||||||
|
private static readonly lootExitEndOfCycle = 0.9;
|
||||||
|
|
||||||
|
static init(deps: PlayerCombatDeps) {
|
||||||
|
this.deps = deps;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get isBusy() {
|
||||||
|
return this.isAttacking || this.isTurningToTarget || this.pendingCombatExit;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get isAutoAttackActive() {
|
||||||
|
return this.isAutoAttacking;
|
||||||
|
}
|
||||||
|
|
||||||
|
static startAutoAttack(
|
||||||
|
onStrike: () => void,
|
||||||
|
_onComplete: () => void,
|
||||||
|
targetWorldPosition: Vector3,
|
||||||
|
) {
|
||||||
|
if (this.isAutoAttacking) return;
|
||||||
|
|
||||||
|
this.isAutoAttacking = true;
|
||||||
|
this.deps.onStopRunning();
|
||||||
|
this.beginTurnToTarget(targetWorldPosition, () => this.playAttack(onStrike));
|
||||||
|
}
|
||||||
|
|
||||||
|
static retargetAutoAttack(targetWorldPosition: Vector3) {
|
||||||
|
if (!this.isAutoAttacking) return;
|
||||||
|
|
||||||
|
this.deps.onSetTurnTarget(targetWorldPosition);
|
||||||
|
this.deps.getPhysicsBody().velocity.set(0, 0, 0);
|
||||||
|
|
||||||
|
if (this.isTurningToTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onTurnComplete = () => {
|
||||||
|
this.deps.onSyncRotation();
|
||||||
|
};
|
||||||
|
this.isTurningToTarget = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static stopAutoAttack() {
|
||||||
|
const wasLooting = this.isLootLoopActive && this.attackPhase === "attacking";
|
||||||
|
const needsExit = this.isAutoAttacking
|
||||||
|
|| this.isTurningToTarget
|
||||||
|
|| this.isAttacking
|
||||||
|
|| this.pendingCombatExit
|
||||||
|
|| this.isBatEquipped;
|
||||||
|
|
||||||
|
this.isAutoAttacking = false;
|
||||||
|
this.onStrike = null;
|
||||||
|
this.cancelTurnToTarget();
|
||||||
|
|
||||||
|
if (wasLooting) {
|
||||||
|
this.pendingCombatExit = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsExit) {
|
||||||
|
this.finishCombatExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static playAttack(onStrike: () => void) {
|
||||||
|
this.onStrike = onStrike;
|
||||||
|
this.isAttacking = true;
|
||||||
|
|
||||||
|
this.deps.getPhysicsBody().velocity.set(0, 0, 0);
|
||||||
|
|
||||||
|
if (!this.isBatEquipped) {
|
||||||
|
this.equipBat();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playBatAttack();
|
||||||
|
}
|
||||||
|
|
||||||
|
static update(delta: number): boolean {
|
||||||
|
if (this.isTurningToTarget) {
|
||||||
|
this.deps.getPhysicsBody().velocity.set(0, 0, 0);
|
||||||
|
this.updateTurnToTarget(delta);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAttacking) {
|
||||||
|
this.deps.getPhysicsBody().velocity.set(0, 0, 0);
|
||||||
|
this.updateAttackStrikes();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static cancelOnMoveInput() {
|
||||||
|
if (this.isAutoAttacking || this.isTurningToTarget) {
|
||||||
|
this.stopAutoAttack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static beginTurnToTarget(targetWorldPosition: Vector3, onComplete: () => void) {
|
||||||
|
this.deps.onSetTurnTarget(targetWorldPosition);
|
||||||
|
|
||||||
|
this.onTurnComplete = onComplete;
|
||||||
|
this.isTurningToTarget = true;
|
||||||
|
this.deps.getPhysicsBody().velocity.set(0, 0, 0);
|
||||||
|
this.deps.character.playAnimation(BaseAnimation.Idle, false, this.turnIdleFade);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static cancelTurnToTarget() {
|
||||||
|
if (!this.isTurningToTarget) return;
|
||||||
|
|
||||||
|
this.isTurningToTarget = false;
|
||||||
|
this.onTurnComplete = null;
|
||||||
|
this.deps.onSyncRotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static updateTurnToTarget(delta: number) {
|
||||||
|
this.deps.onUpdateRotation(delta);
|
||||||
|
|
||||||
|
if (!this.deps.isRotationComplete()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isTurningToTarget = false;
|
||||||
|
this.deps.onSyncRotation();
|
||||||
|
|
||||||
|
const callback = this.onTurnComplete;
|
||||||
|
this.onTurnComplete = null;
|
||||||
|
callback?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static playBatAttack() {
|
||||||
|
if (this.isLootLoopActive && this.isAutoAttacking) return;
|
||||||
|
|
||||||
|
this.startLootLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static startLootLoop() {
|
||||||
|
const animId = this.getBatAttackAnimation();
|
||||||
|
if (animId !== BaseAnimation.Loot) {
|
||||||
|
this.attackPhase = "attacking";
|
||||||
|
this.deps.character.playAnimation(animId, false, this.lootEnterFade);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.attackPhase = "attacking";
|
||||||
|
this.strikeMarkIndex = 0;
|
||||||
|
this.prevStrikeNormTime = 0;
|
||||||
|
this.isLootLoopActive = true;
|
||||||
|
this.deps.character.playAnimation(BaseAnimation.Loot, false, this.lootEnterFade);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static updateAttackStrikes() {
|
||||||
|
if (this.attackPhase !== "attacking") return;
|
||||||
|
|
||||||
|
const action = this.deps.character.curClipAction;
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
const clipDuration = action.getClip().duration;
|
||||||
|
if (clipDuration <= 0) return;
|
||||||
|
|
||||||
|
const normalizedTime = action.time / clipDuration;
|
||||||
|
|
||||||
|
if (normalizedTime < this.prevStrikeNormTime) {
|
||||||
|
if (this.pendingCombatExit) {
|
||||||
|
this.finishCombatExit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.strikeMarkIndex = 0;
|
||||||
|
}
|
||||||
|
this.prevStrikeNormTime = normalizedTime;
|
||||||
|
|
||||||
|
while (
|
||||||
|
this.strikeMarkIndex < this.attackStrikeMarks.length &&
|
||||||
|
normalizedTime >= this.attackStrikeMarks[this.strikeMarkIndex]
|
||||||
|
) {
|
||||||
|
this.onStrike?.();
|
||||||
|
this.strikeMarkIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pendingCombatExit && this.canExitLootNow(normalizedTime)) {
|
||||||
|
this.finishCombatExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static canExitLootNow(normalizedTime: number) {
|
||||||
|
if (this.strikeMarkIndex >= 2) {
|
||||||
|
return normalizedTime >= this.lootExitAfterSecondStrike;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.strikeMarkIndex >= 1) {
|
||||||
|
return normalizedTime >= this.lootExitAfterFirstStrike;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedTime >= this.lootExitEndOfCycle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getBatAttackAnimation() {
|
||||||
|
if (this.deps.character.animationList.length > BaseAnimation.Loot) {
|
||||||
|
return BaseAnimation.Loot;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BaseAnimation.Idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static equipBat() {
|
||||||
|
this.isBatEquipped = true;
|
||||||
|
this.deps.character.setBatEquipped(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static finishCombatExit() {
|
||||||
|
this.pendingCombatExit = false;
|
||||||
|
this.attackPhase = "none";
|
||||||
|
this.isAttacking = false;
|
||||||
|
this.strikeMarkIndex = 0;
|
||||||
|
this.prevStrikeNormTime = 0;
|
||||||
|
this.isLootLoopActive = false;
|
||||||
|
this.isBatEquipped = false;
|
||||||
|
this.deps.character.setDefaultWeapons();
|
||||||
|
this.deps.onSyncRotation();
|
||||||
|
|
||||||
|
if (this.deps.isMoving()) {
|
||||||
|
this.deps.onResumeRunning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deps.character.crossFadeToAnimation(BaseAnimation.Idle, false, this.combatExitFade);
|
||||||
|
this.deps.onStopRunning();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ResourceType } from "./ResourceType";
|
||||||
|
|
||||||
function randomInt(min: number, max: number) {
|
function randomInt(min: number, max: number) {
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
}
|
}
|
||||||
@@ -26,8 +28,6 @@ function splitRandom(total: number, parts: number) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { ResourceType } from "./ResourceType";
|
|
||||||
|
|
||||||
export type PropDropPlan = {
|
export type PropDropPlan = {
|
||||||
resource: ResourceType;
|
resource: ResourceType;
|
||||||
totalDrop: number;
|
totalDrop: number;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const PROP_DROPS: Record<PropType, DropRule[]> = {
|
|||||||
{
|
{
|
||||||
resource: ResourceType.Wood,
|
resource: ResourceType.Wood,
|
||||||
minTotal: 1,
|
minTotal: 1,
|
||||||
maxTotal: 3,
|
maxTotal: 4,
|
||||||
minSpawnHits: 1,
|
minSpawnHits: 1,
|
||||||
maxSpawnHits: 2,
|
maxSpawnHits: 2,
|
||||||
},
|
},
|
||||||
@@ -27,5 +27,6 @@ export function resolvePropType(objectName: string): PropType {
|
|||||||
return PropType.Box;
|
return PropType.Box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.warn(`Unknown prop type for "${objectName}", falling back to Box`);
|
||||||
return PropType.Box;
|
return PropType.Box;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ResourceType } from "./ResourceType";
|
||||||
|
|
||||||
|
export const RESOURCE_PLACEHOLDER_COLORS: Record<ResourceType, string> = {
|
||||||
|
[ResourceType.Wood]: "#6b4423",
|
||||||
|
};
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { UpdateController } from "@24tools/playable_template";
|
||||||
|
import { Group } from "@tweenjs/tween.js";
|
||||||
|
import { GatherC } from "../Map/GatherC";
|
||||||
|
import { DepositZoneC } from "../Map/DepositZoneC";
|
||||||
|
import { InteractiveZoneC } from "../Map/InteractiveZoneC";
|
||||||
|
import { Player } from "../Presets/Player";
|
||||||
|
import { ResourceInventoryC } from "./ResourceInventoryC";
|
||||||
|
import { ResourceScreenFly } from "./ResourceScreenFly";
|
||||||
|
import { ResourceType } from "./ResourceType";
|
||||||
|
import { ResourceUIC } from "./ResourceUIC";
|
||||||
|
|
||||||
|
const DEPOSIT_FLY_MS = 520;
|
||||||
|
const DEPOSIT_FLY_SCALE = 0.35;
|
||||||
|
const CHAIN_DEPOSIT_DELAY_MS = 140;
|
||||||
|
|
||||||
|
export class ResourceDepositC {
|
||||||
|
private static inited = false;
|
||||||
|
private static tweenGroup = new Group();
|
||||||
|
private static isProcessing = false;
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
if (this.inited) return;
|
||||||
|
this.inited = true;
|
||||||
|
|
||||||
|
DepositZoneC.onPlayerEnter.addDelegate(() => {
|
||||||
|
this.tryStart();
|
||||||
|
});
|
||||||
|
|
||||||
|
GatherC.onCombatIdle.addDelegate(() => {
|
||||||
|
this.tryStart();
|
||||||
|
});
|
||||||
|
|
||||||
|
ResourceInventoryC.onChanged.addDelegate(() => {
|
||||||
|
this.tryStart();
|
||||||
|
});
|
||||||
|
|
||||||
|
UpdateController.Instance.onUpdate.addDelegate(() => {
|
||||||
|
this.tweenGroup.update(performance.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static tryStart() {
|
||||||
|
if (!this.canDeposit()) return;
|
||||||
|
this.processNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static canDeposit() {
|
||||||
|
return DepositZoneC.isPlayerInside
|
||||||
|
&& !Player.isAutoAttackActive()
|
||||||
|
&& !Player.isCombatBusy()
|
||||||
|
&& this.getDepositableType() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getDepositableType() {
|
||||||
|
for (const type of Object.values(ResourceType)) {
|
||||||
|
if (ResourceInventoryC.get(type) > 0) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static processNext() {
|
||||||
|
if (this.isProcessing) return;
|
||||||
|
|
||||||
|
const type = this.getDepositableType();
|
||||||
|
if (!type || !this.canDeposit()) {
|
||||||
|
this.isProcessing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconCenter = ResourceUIC.getIconCenter(type);
|
||||||
|
const zoneScreen = InteractiveZoneC.getScreenCenter();
|
||||||
|
if (!iconCenter) return;
|
||||||
|
|
||||||
|
const pickup = ResourceScreenFly.createPickup(type);
|
||||||
|
if (!pickup) return;
|
||||||
|
|
||||||
|
this.isProcessing = true;
|
||||||
|
|
||||||
|
const startScreen = ResourceScreenFly.screenPointFromClient(
|
||||||
|
iconCenter.x,
|
||||||
|
iconCenter.y,
|
||||||
|
zoneScreen.z,
|
||||||
|
);
|
||||||
|
|
||||||
|
ResourceScreenFly.flyAlongScreen(
|
||||||
|
pickup,
|
||||||
|
startScreen,
|
||||||
|
zoneScreen,
|
||||||
|
this.tweenGroup,
|
||||||
|
DEPOSIT_FLY_MS,
|
||||||
|
DEPOSIT_FLY_SCALE,
|
||||||
|
() => {
|
||||||
|
ResourceInventoryC.remove(type, 1);
|
||||||
|
ResourceUIC.refresh(type);
|
||||||
|
InteractiveZoneC.addDeposit(1);
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.isProcessing = false;
|
||||||
|
this.processNext();
|
||||||
|
}, CHAIN_DEPOSIT_DELAY_MS);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,121 +1,206 @@
|
|||||||
import { UpdateController } from "@24tools/playable_template";
|
import { UpdateController } from "@24tools/playable_template";
|
||||||
import { Easing, Group, Tween } from "@tweenjs/tween.js";
|
import { Easing, Group, Tween } from "@tweenjs/tween.js";
|
||||||
import {
|
import { Object3D, Vector3 } from "three";
|
||||||
BoxGeometry,
|
|
||||||
Mesh,
|
|
||||||
MeshBasicMaterial,
|
|
||||||
Vector3,
|
|
||||||
} from "three";
|
|
||||||
import { CameraC } from "../CameraC";
|
|
||||||
import { ThreeC } from "../ThreeC";
|
import { ThreeC } from "../ThreeC";
|
||||||
import { ResourceInventoryC } from "./ResourceInventoryC";
|
import { ResourceInventoryC } from "./ResourceInventoryC";
|
||||||
|
import { ResourceScreenFly } from "./ResourceScreenFly";
|
||||||
import { ResourceType } from "./ResourceType";
|
import { ResourceType } from "./ResourceType";
|
||||||
import { ResourceUIC } from "./ResourceUIC";
|
import { ResourceUIC } from "./ResourceUIC";
|
||||||
|
|
||||||
const POP_DURATION_MS = 220;
|
const FALL_MS = 300;
|
||||||
|
const EJECT_MS = 180;
|
||||||
|
const SPAWN_HEIGHT = 0.55;
|
||||||
|
const SCATTER_MIN = 1;
|
||||||
|
const SCATTER_MAX = 1.05;
|
||||||
|
const BOUNCE_HEIGHTS = [0.22, 0.09];
|
||||||
|
const BOUNCE_UP_MS = 200;
|
||||||
|
const BOUNCE_DOWN_MS = 180;
|
||||||
|
const REST_AFTER_BOUNCE_MS = 400;
|
||||||
const FLY_DURATION_MS = 520;
|
const FLY_DURATION_MS = 520;
|
||||||
const PICKUP_SIZE = 0.14;
|
const CHAIN_FLY_DELAY_MS = 140;
|
||||||
|
const FLY_SCALE = 0.35;
|
||||||
const STAGGER_MS = 70;
|
const STAGGER_MS = 70;
|
||||||
|
const GROUND_LIFT = 0.08;
|
||||||
|
|
||||||
const PLACEHOLDER_COLORS: Record<ResourceType, string> = {
|
type FlyQueueItem = {
|
||||||
[ResourceType.Wood]: "#6b4423",
|
pickup: Object3D;
|
||||||
|
type: ResourceType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export class ResourceFlyC {
|
export class ResourceFlyC {
|
||||||
private static inited = false;
|
private static inited = false;
|
||||||
private static tweenGroup = new Group();
|
private static tweenGroup = new Group();
|
||||||
|
private static flyQueue: FlyQueueItem[] = [];
|
||||||
|
private static isProcessingFlyQueue = false;
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
if (this.inited) return;
|
if (this.inited) return;
|
||||||
this.inited = true;
|
this.inited = true;
|
||||||
|
|
||||||
|
this.hideTemplateMeshes();
|
||||||
|
|
||||||
UpdateController.Instance.onUpdate.addDelegate(() => {
|
UpdateController.Instance.onUpdate.addDelegate(() => {
|
||||||
this.tweenGroup.update(performance.now());
|
this.tweenGroup.update(performance.now());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static launch(origin: Vector3, type: ResourceType, index: number) {
|
private static hideTemplateMeshes() {
|
||||||
const spawnPos = origin.clone().add(
|
Object.values(ResourceType).forEach((type) => {
|
||||||
new Vector3(
|
const template = ThreeC.getObject(type);
|
||||||
(Math.random() - 0.5) * 0.35,
|
if (!template) return;
|
||||||
0.25 + Math.random() * 0.2,
|
|
||||||
(Math.random() - 0.5) * 0.35,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
template.visible = false;
|
||||||
|
template.removeFromParent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static launch(origin: Vector3, type: ResourceType, index: number, count: number) {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.startPickup(spawnPos, type);
|
this.startPickup(origin, type, index, count);
|
||||||
}, index * STAGGER_MS);
|
}, index * STAGGER_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static startPickup(worldPos: Vector3, type: ResourceType) {
|
private static startPickup(origin: Vector3, type: ResourceType, index: number, count: number) {
|
||||||
const mesh = this.createWorldMesh(type);
|
const pickup = ResourceScreenFly.createPickup(type);
|
||||||
mesh.position.copy(worldPos);
|
if (!pickup) return;
|
||||||
ThreeC.addToScene(mesh);
|
|
||||||
|
|
||||||
const popTarget = worldPos.clone().add(new Vector3(0, 0.35, 0));
|
const scatter = this.getScatterOffset(index, count);
|
||||||
|
const landPos = origin.clone().add(scatter);
|
||||||
|
landPos.y += GROUND_LIFT;
|
||||||
|
const spawnPos = origin.clone().add(new Vector3(0, SPAWN_HEIGHT * 0.25, 0));
|
||||||
|
const peakPos = origin.clone()
|
||||||
|
.add(scatter.clone().multiplyScalar(0.65))
|
||||||
|
.add(new Vector3(0, SPAWN_HEIGHT, 0));
|
||||||
|
|
||||||
const popTween = new Tween(mesh.position)
|
pickup.position.copy(spawnPos);
|
||||||
.to({ x: popTarget.x, y: popTarget.y, z: popTarget.z }, POP_DURATION_MS)
|
ResourceScreenFly.orientToCamera(pickup);
|
||||||
|
ThreeC.addToScene(pickup);
|
||||||
|
|
||||||
|
const ejectTween = new Tween(pickup.position)
|
||||||
|
.to({ x: peakPos.x, y: peakPos.y, z: peakPos.z }, EJECT_MS)
|
||||||
.easing(Easing.Quadratic.Out)
|
.easing(Easing.Quadratic.Out)
|
||||||
.onComplete(() => {
|
.onUpdate(() => {
|
||||||
ThreeC.removeFromScene(mesh);
|
ResourceScreenFly.orientToCamera(pickup);
|
||||||
mesh.geometry.dispose();
|
|
||||||
(mesh.material as MeshBasicMaterial).dispose();
|
|
||||||
this.flyToUI(popTarget, type);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tweenGroup.add(popTween);
|
const fallTween = new Tween(pickup.position)
|
||||||
popTween.start(performance.now());
|
.to({ x: landPos.x, y: landPos.y, z: landPos.z }, FALL_MS)
|
||||||
}
|
|
||||||
|
|
||||||
private static flyToUI(fromWorldPos: Vector3, type: ResourceType) {
|
|
||||||
const start = this.worldToScreen(fromWorldPos);
|
|
||||||
const target = ResourceUIC.getIconCenter(type);
|
|
||||||
if (!target) return;
|
|
||||||
|
|
||||||
const element = document.createElement("div");
|
|
||||||
element.className = "resource-fly-pickup";
|
|
||||||
element.style.backgroundColor = PLACEHOLDER_COLORS[type];
|
|
||||||
element.style.left = `${start.x}px`;
|
|
||||||
element.style.top = `${start.y}px`;
|
|
||||||
document.body.appendChild(element);
|
|
||||||
|
|
||||||
const state = { x: start.x, y: start.y, scale: 1 };
|
|
||||||
|
|
||||||
const flyTween = new Tween(state)
|
|
||||||
.to({ x: target.x, y: target.y, scale: 0.35 }, FLY_DURATION_MS)
|
|
||||||
.easing(Easing.Quadratic.In)
|
.easing(Easing.Quadratic.In)
|
||||||
.onUpdate(() => {
|
.onUpdate(() => {
|
||||||
element.style.left = `${state.x}px`;
|
ResourceScreenFly.orientToCamera(pickup);
|
||||||
element.style.top = `${state.y}px`;
|
|
||||||
element.style.transform = `translate(-50%, -50%) scale(${state.scale})`;
|
|
||||||
})
|
})
|
||||||
.onComplete(() => {
|
.onComplete(() => {
|
||||||
element.remove();
|
this.playBounces(pickup, landPos, type);
|
||||||
ResourceInventoryC.add(type, 1);
|
|
||||||
ResourceUIC.refresh(type);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tweenGroup.add(flyTween);
|
ejectTween.chain(fallTween);
|
||||||
flyTween.start(performance.now());
|
this.tweenGroup.add(ejectTween);
|
||||||
|
this.tweenGroup.add(fallTween);
|
||||||
|
ejectTween.start(performance.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static createWorldMesh(type: ResourceType) {
|
private static getScatterOffset(index: number, count: number) {
|
||||||
const color = PLACEHOLDER_COLORS[type];
|
const baseAngle = (index / Math.max(count, 1)) * Math.PI * 2;
|
||||||
return new Mesh(
|
const angle = baseAngle + (Math.random() - 0.5) * 0.7;
|
||||||
new BoxGeometry(PICKUP_SIZE, PICKUP_SIZE, PICKUP_SIZE),
|
const distance = SCATTER_MIN + Math.random() * (SCATTER_MAX - SCATTER_MIN);
|
||||||
new MeshBasicMaterial({ color }),
|
|
||||||
|
return new Vector3(
|
||||||
|
Math.cos(angle) * distance,
|
||||||
|
0,
|
||||||
|
Math.sin(angle) * distance,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static worldToScreen(worldPos: Vector3) {
|
private static playBounces(pickup: Object3D, landPos: Vector3, type: ResourceType) {
|
||||||
const camera = CameraC.camera;
|
const runBounce = (bounceIndex: number) => {
|
||||||
const projected = worldPos.clone().project(camera);
|
if (bounceIndex >= BOUNCE_HEIGHTS.length) {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.enqueueFlyToUI(pickup, type);
|
||||||
|
}, REST_AFTER_BOUNCE_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
const height = BOUNCE_HEIGHTS[bounceIndex];
|
||||||
x: (projected.x * 0.5 + 0.5) * window.innerWidth,
|
const durationScale = height / BOUNCE_HEIGHTS[0];
|
||||||
y: (-projected.y * 0.5 + 0.5) * window.innerHeight,
|
const peakY = landPos.y + height;
|
||||||
|
|
||||||
|
const upTween = new Tween(pickup.position)
|
||||||
|
.to({ y: peakY }, BOUNCE_UP_MS * durationScale)
|
||||||
|
.easing(Easing.Quadratic.Out)
|
||||||
|
.onUpdate(() => {
|
||||||
|
ResourceScreenFly.orientToCamera(pickup);
|
||||||
|
});
|
||||||
|
|
||||||
|
const downTween = new Tween(pickup.position)
|
||||||
|
.to({ y: landPos.y }, BOUNCE_DOWN_MS * durationScale)
|
||||||
|
.easing(Easing.Quadratic.In)
|
||||||
|
.onUpdate(() => {
|
||||||
|
ResourceScreenFly.orientToCamera(pickup);
|
||||||
|
})
|
||||||
|
.onComplete(() => {
|
||||||
|
runBounce(bounceIndex + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
upTween.chain(downTween);
|
||||||
|
this.tweenGroup.add(upTween);
|
||||||
|
this.tweenGroup.add(downTween);
|
||||||
|
upTween.start(performance.now());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
runBounce(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static enqueueFlyToUI(pickup: Object3D, type: ResourceType) {
|
||||||
|
this.flyQueue.push({ pickup, type });
|
||||||
|
this.processFlyQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static processFlyQueue() {
|
||||||
|
if (this.isProcessingFlyQueue || this.flyQueue.length === 0) return;
|
||||||
|
|
||||||
|
this.isProcessingFlyQueue = true;
|
||||||
|
const item = this.flyQueue.shift();
|
||||||
|
if (!item) {
|
||||||
|
this.isProcessingFlyQueue = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.flyToUI(item.pickup, item.type, () => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.isProcessingFlyQueue = false;
|
||||||
|
this.processFlyQueue();
|
||||||
|
}, CHAIN_FLY_DELAY_MS);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static flyToUI(pickup: Object3D, type: ResourceType, onComplete: () => void) {
|
||||||
|
const iconCenter = ResourceUIC.getIconCenter(type);
|
||||||
|
if (!iconCenter) {
|
||||||
|
ThreeC.removeFromScene(pickup);
|
||||||
|
onComplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startScreen = ResourceScreenFly.worldToScreen(pickup.position);
|
||||||
|
const endScreen = ResourceScreenFly.screenPointFromClient(
|
||||||
|
iconCenter.x,
|
||||||
|
iconCenter.y,
|
||||||
|
startScreen.z,
|
||||||
|
);
|
||||||
|
|
||||||
|
ResourceScreenFly.flyAlongScreen(
|
||||||
|
pickup,
|
||||||
|
startScreen,
|
||||||
|
endScreen,
|
||||||
|
this.tweenGroup,
|
||||||
|
FLY_DURATION_MS,
|
||||||
|
FLY_SCALE,
|
||||||
|
() => {
|
||||||
|
ResourceInventoryC.add(type, 1);
|
||||||
|
ResourceUIC.refresh(type);
|
||||||
|
onComplete();
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { EasyEvent } from "@24tools/playable_template";
|
||||||
import { ResourceType } from "./ResourceType";
|
import { ResourceType } from "./ResourceType";
|
||||||
|
|
||||||
export class ResourceInventoryC {
|
export class ResourceInventoryC {
|
||||||
private static amounts = new Map<ResourceType, number>();
|
private static amounts = new Map<ResourceType, number>();
|
||||||
|
static readonly onChanged = new EasyEvent<{ type: ResourceType }>();
|
||||||
|
|
||||||
static get(type: ResourceType) {
|
static get(type: ResourceType) {
|
||||||
return this.amounts.get(type) ?? 0;
|
return this.amounts.get(type) ?? 0;
|
||||||
@@ -10,5 +12,12 @@ export class ResourceInventoryC {
|
|||||||
static add(type: ResourceType, amount: number) {
|
static add(type: ResourceType, amount: number) {
|
||||||
if (amount <= 0) return;
|
if (amount <= 0) return;
|
||||||
this.amounts.set(type, this.get(type) + amount);
|
this.amounts.set(type, this.get(type) + amount);
|
||||||
|
this.onChanged.Invoke({ type });
|
||||||
|
}
|
||||||
|
|
||||||
|
static remove(type: ResourceType, amount: number) {
|
||||||
|
if (amount <= 0) return;
|
||||||
|
this.amounts.set(type, Math.max(0, this.get(type) - amount));
|
||||||
|
this.onChanged.Invoke({ type });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { ResourcesC } from "@24tools/playable_template";
|
||||||
|
import { Easing, Group, Tween } from "@tweenjs/tween.js";
|
||||||
|
import {
|
||||||
|
Material,
|
||||||
|
Mesh,
|
||||||
|
Object3D,
|
||||||
|
Vector3,
|
||||||
|
} from "three";
|
||||||
|
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
|
||||||
|
import { CameraC } from "../CameraC";
|
||||||
|
import { ResourcesType } from "../Presets/Enums/ResourcesType";
|
||||||
|
import { ThreeC } from "../ThreeC";
|
||||||
|
import { ResourceType } from "./ResourceType";
|
||||||
|
|
||||||
|
export type ScreenPoint = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PICKUP_SIZE = 1;
|
||||||
|
|
||||||
|
export class ResourceScreenFly {
|
||||||
|
static createPickup(type: ResourceType) {
|
||||||
|
const prefab = ResourcesC.getResource<GLTF>(ResourcesType.Mesh, type);
|
||||||
|
if (!prefab?.scene) {
|
||||||
|
console.warn(`Resource mesh not found: ${type}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meshSource = this.findFirstMesh(prefab.scene);
|
||||||
|
if (!meshSource) {
|
||||||
|
console.warn(`Resource mesh has no geometry: ${type}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mesh = meshSource.clone();
|
||||||
|
mesh.position.set(0, 0, 0);
|
||||||
|
mesh.rotation.set(0, 0, 0);
|
||||||
|
mesh.scale.set(1, 1, 1);
|
||||||
|
this.cloneMaterial(mesh);
|
||||||
|
|
||||||
|
const pickup = new Object3D();
|
||||||
|
pickup.add(mesh);
|
||||||
|
|
||||||
|
mesh.geometry.computeBoundingBox();
|
||||||
|
const size = new Vector3();
|
||||||
|
mesh.geometry.boundingBox!.getSize(size);
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
if (maxDim > 0) {
|
||||||
|
pickup.scale.setScalar(PICKUP_SIZE / maxDim);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.orientToCamera(pickup);
|
||||||
|
return pickup;
|
||||||
|
}
|
||||||
|
|
||||||
|
static flyAlongScreen(
|
||||||
|
pickup: Object3D,
|
||||||
|
startScreen: ScreenPoint,
|
||||||
|
endScreen: ScreenPoint,
|
||||||
|
tweenGroup: Group,
|
||||||
|
durationMs: number,
|
||||||
|
endScale: number,
|
||||||
|
onComplete: () => void,
|
||||||
|
) {
|
||||||
|
const startScale = pickup.scale.clone();
|
||||||
|
pickup.position.copy(this.screenToWorld(startScreen));
|
||||||
|
this.orientToCamera(pickup);
|
||||||
|
ThreeC.addToScene(pickup);
|
||||||
|
|
||||||
|
const state = { t: 0 };
|
||||||
|
|
||||||
|
const flyTween = new Tween(state)
|
||||||
|
.to({ t: 1 }, durationMs)
|
||||||
|
.easing(Easing.Cubic.InOut)
|
||||||
|
.onUpdate(() => {
|
||||||
|
const screen = this.lerpScreenPoint(startScreen, endScreen, state.t);
|
||||||
|
pickup.position.copy(this.screenToWorld(screen));
|
||||||
|
const scale = this.lerp(1, endScale, state.t);
|
||||||
|
pickup.scale.set(
|
||||||
|
startScale.x * scale,
|
||||||
|
startScale.y * scale,
|
||||||
|
startScale.z * scale,
|
||||||
|
);
|
||||||
|
this.orientToCamera(pickup);
|
||||||
|
})
|
||||||
|
.onComplete(() => {
|
||||||
|
ThreeC.removeFromScene(pickup);
|
||||||
|
onComplete();
|
||||||
|
});
|
||||||
|
|
||||||
|
tweenGroup.add(flyTween);
|
||||||
|
flyTween.start(performance.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
static orientToCamera(object: Object3D) {
|
||||||
|
object.quaternion.copy(CameraC.camera.quaternion);
|
||||||
|
}
|
||||||
|
|
||||||
|
static worldToScreen(worldPos: Vector3): ScreenPoint {
|
||||||
|
const canvasRect = this.getCanvasRect();
|
||||||
|
if (!canvasRect) {
|
||||||
|
return { x: 0, y: 0, z: 0.5 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const projected = worldPos.clone().project(CameraC.camera);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: (projected.x * 0.5 + 0.5) * canvasRect.width + canvasRect.left,
|
||||||
|
y: (-projected.y * 0.5 + 0.5) * canvasRect.height + canvasRect.top,
|
||||||
|
z: projected.z,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static screenPointFromClient(clientX: number, clientY: number, ndcZ: number): ScreenPoint {
|
||||||
|
return { x: clientX, y: clientY, z: ndcZ };
|
||||||
|
}
|
||||||
|
|
||||||
|
static screenToWorld(screen: ScreenPoint) {
|
||||||
|
const canvasRect = this.getCanvasRect();
|
||||||
|
const camera = CameraC.camera;
|
||||||
|
if (!canvasRect) {
|
||||||
|
return new Vector3();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ndc = new Vector3(
|
||||||
|
((screen.x - canvasRect.left) / canvasRect.width) * 2 - 1,
|
||||||
|
-((screen.y - canvasRect.top) / canvasRect.height) * 2 + 1,
|
||||||
|
screen.z,
|
||||||
|
);
|
||||||
|
ndc.unproject(camera);
|
||||||
|
return ndc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static lerp(from: number, to: number, t: number) {
|
||||||
|
return from + (to - from) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static lerpScreenPoint(from: ScreenPoint, to: ScreenPoint, t: number): ScreenPoint {
|
||||||
|
return {
|
||||||
|
x: this.lerp(from.x, to.x, t),
|
||||||
|
y: this.lerp(from.y, to.y, t),
|
||||||
|
z: this.lerp(from.z, to.z, t),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getCanvasRect() {
|
||||||
|
const canvas = document.querySelector("canvas");
|
||||||
|
return canvas?.getBoundingClientRect() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static findFirstMesh(root: Object3D): Mesh | null {
|
||||||
|
let result: Mesh | null = null;
|
||||||
|
root.traverse((child) => {
|
||||||
|
if (!result && (child as Mesh).isMesh) {
|
||||||
|
result = child as Mesh;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static cloneMaterial(mesh: Mesh) {
|
||||||
|
const applyPickupMaterial = (material: Material) => {
|
||||||
|
const cloned = material.clone();
|
||||||
|
cloned.depthTest = true;
|
||||||
|
cloned.depthWrite = true;
|
||||||
|
cloned.polygonOffset = true;
|
||||||
|
cloned.polygonOffsetFactor = -2;
|
||||||
|
cloned.polygonOffsetUnits = -2;
|
||||||
|
return cloned;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { material } = mesh;
|
||||||
|
if (Array.isArray(material)) {
|
||||||
|
mesh.material = material.map((entry) => applyPickupMaterial(entry));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (material) {
|
||||||
|
mesh.material = applyPickupMaterial(material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ export class ResourceSpawnC {
|
|||||||
const origin = prop.object.getWorldPosition(new Vector3());
|
const origin = prop.object.getWorldPosition(new Vector3());
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
ResourceFlyC.launch(origin, type, i);
|
ResourceFlyC.launch(origin, type, i, count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
|
import { UI_IMAGES } from "../../resources/images/uiImages";
|
||||||
import { ResourceInventoryC } from "./ResourceInventoryC";
|
import { ResourceInventoryC } from "./ResourceInventoryC";
|
||||||
import { ResourceType } from "./ResourceType";
|
import { ResourceType } from "./ResourceType";
|
||||||
|
import { RESOURCE_PLACEHOLDER_COLORS } from "./ResourceConfig";
|
||||||
|
|
||||||
type ResourceUIEntry = {
|
type ResourceUIEntry = {
|
||||||
type: ResourceType;
|
type: ResourceType;
|
||||||
|
counter: HTMLElement;
|
||||||
icon: HTMLElement;
|
icon: HTMLElement;
|
||||||
count: HTMLElement;
|
count: HTMLElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PLACEHOLDER_COLORS: Record<ResourceType, string> = {
|
|
||||||
[ResourceType.Wood]: "#6b4423",
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ResourceUIC {
|
export class ResourceUIC {
|
||||||
private static entries = new Map<ResourceType, ResourceUIEntry>();
|
private static entries = new Map<ResourceType, ResourceUIEntry>();
|
||||||
private static root: HTMLElement | null = null;
|
private static root: HTMLElement | null = null;
|
||||||
@@ -19,34 +18,78 @@ export class ResourceUIC {
|
|||||||
const uiRoot = document.getElementById("ui");
|
const uiRoot = document.getElementById("ui");
|
||||||
if (!uiRoot) return;
|
if (!uiRoot) return;
|
||||||
|
|
||||||
|
this.createTopRightHud(uiRoot);
|
||||||
|
this.createBottomLeftWeapon(uiRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createTopRightHud(uiRoot: HTMLElement) {
|
||||||
|
const topRight = document.createElement("div");
|
||||||
|
topRight.className = "hud-top-right";
|
||||||
|
|
||||||
|
const avatar = document.createElement("img");
|
||||||
|
avatar.className = "hud-avatar";
|
||||||
|
avatar.src = UI_IMAGES.avatarTop;
|
||||||
|
avatar.alt = "";
|
||||||
|
topRight.appendChild(avatar);
|
||||||
|
|
||||||
this.root = document.createElement("div");
|
this.root = document.createElement("div");
|
||||||
this.root.id = "resource-bar";
|
this.root.id = "resource-bar";
|
||||||
this.root.className = "resource-bar";
|
this.root.className = "resource-bar";
|
||||||
uiRoot.appendChild(this.root);
|
topRight.appendChild(this.root);
|
||||||
|
|
||||||
|
uiRoot.appendChild(topRight);
|
||||||
this.register(ResourceType.Wood);
|
this.register(ResourceType.Wood);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static createBottomLeftWeapon(uiRoot: HTMLElement) {
|
||||||
|
const bottomLeft = document.createElement("div");
|
||||||
|
bottomLeft.className = "hud-bottom-left";
|
||||||
|
|
||||||
|
const weapon = document.createElement("div");
|
||||||
|
weapon.className = "hud-weapon";
|
||||||
|
|
||||||
|
const bg = document.createElement("img");
|
||||||
|
bg.className = "hud-weapon__bg";
|
||||||
|
bg.src = UI_IMAGES.toolBg;
|
||||||
|
bg.alt = "";
|
||||||
|
|
||||||
|
const tool = document.createElement("img");
|
||||||
|
tool.className = "hud-weapon__tool";
|
||||||
|
tool.src = UI_IMAGES.toolBat;
|
||||||
|
tool.alt = "";
|
||||||
|
|
||||||
|
const level = document.createElement("span");
|
||||||
|
level.className = "hud-weapon__level";
|
||||||
|
level.textContent = "LVL 0";
|
||||||
|
|
||||||
|
weapon.appendChild(bg);
|
||||||
|
weapon.appendChild(tool);
|
||||||
|
weapon.appendChild(level);
|
||||||
|
bottomLeft.appendChild(weapon);
|
||||||
|
uiRoot.appendChild(bottomLeft);
|
||||||
|
}
|
||||||
|
|
||||||
static register(type: ResourceType) {
|
static register(type: ResourceType) {
|
||||||
if (!this.root || this.entries.has(type)) return;
|
if (!this.root || this.entries.has(type)) return;
|
||||||
|
|
||||||
const counter = document.createElement("div");
|
const counter = document.createElement("div");
|
||||||
counter.className = "resource-counter";
|
counter.className = "resource-counter";
|
||||||
counter.dataset.resource = type;
|
counter.dataset.resource = type;
|
||||||
|
counter.style.backgroundImage = `url(${UI_IMAGES.woodCounterBg})`;
|
||||||
const icon = document.createElement("div");
|
|
||||||
icon.className = "resource-icon";
|
|
||||||
icon.style.backgroundColor = PLACEHOLDER_COLORS[type];
|
|
||||||
|
|
||||||
const count = document.createElement("span");
|
const count = document.createElement("span");
|
||||||
count.className = "resource-count";
|
count.className = "resource-count";
|
||||||
count.textContent = "0";
|
count.textContent = "0";
|
||||||
|
|
||||||
counter.appendChild(icon);
|
const icon = document.createElement("div");
|
||||||
|
icon.className = "resource-icon";
|
||||||
|
icon.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
counter.appendChild(count);
|
counter.appendChild(count);
|
||||||
|
counter.appendChild(icon);
|
||||||
this.root.appendChild(counter);
|
this.root.appendChild(counter);
|
||||||
|
|
||||||
this.entries.set(type, { type, icon, count });
|
this.entries.set(type, { type, counter, icon, count });
|
||||||
this.refresh(type);
|
this.refresh(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +97,7 @@ export class ResourceUIC {
|
|||||||
const entry = this.entries.get(type);
|
const entry = this.entries.get(type);
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
|
|
||||||
const rect = entry.icon.getBoundingClientRect();
|
const rect = entry.counter.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
x: rect.left + rect.width / 2,
|
x: rect.left + rect.width / 2,
|
||||||
y: rect.top + rect.height / 2,
|
y: rect.top + rect.height / 2,
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
import { Vector3 } from "three";
|
import { ThreeC } from "./ThreeC";
|
||||||
import { Player } from "./Presets/Player";
|
import { Player } from "./Presets/Player";
|
||||||
import { Map } from "./Map/Map";
|
import { Map } from "./Map/Map";
|
||||||
import { GatherC } from "./Map/GatherC";
|
import { GatherC } from "./Map/GatherC";
|
||||||
|
import { DepositZoneC } from "./Map/DepositZoneC";
|
||||||
import { ResourceUIC } from "./Resources/ResourceUIC";
|
import { ResourceUIC } from "./Resources/ResourceUIC";
|
||||||
import { ResourceFlyC } from "./Resources/ResourceFlyC";
|
import { ResourceFlyC } from "./Resources/ResourceFlyC";
|
||||||
|
import { ResourceDepositC } from "./Resources/ResourceDepositC";
|
||||||
|
import { PropHpUIC } from "./Map/PropHpUIC";
|
||||||
|
import { PropVfxC } from "./Map/PropVfxC";
|
||||||
|
import { InvasionProgressUIC } from "./UI/InvasionProgressUIC";
|
||||||
|
|
||||||
export class TestSceneC {
|
export class TestSceneC {
|
||||||
static init() {
|
static init() {
|
||||||
|
InvasionProgressUIC.init();
|
||||||
ResourceUIC.init();
|
ResourceUIC.init();
|
||||||
ResourceFlyC.init();
|
ResourceFlyC.init();
|
||||||
Map.Init();
|
PropHpUIC.init();
|
||||||
this.InitPlayer();
|
PropVfxC.Init();
|
||||||
|
Map.init();
|
||||||
GatherC.init();
|
GatherC.init();
|
||||||
|
Player.init();
|
||||||
|
|
||||||
|
const mapObject = ThreeC.getObject("map");
|
||||||
|
if (mapObject) {
|
||||||
|
DepositZoneC.init(mapObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InitPlayer() {
|
ResourceDepositC.init();
|
||||||
const playerSpawnPoint = new Vector3(4, 0, 27);
|
|
||||||
Player.SetSpawnPosition(playerSpawnPoint);
|
|
||||||
Player.Init();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,42 +65,4 @@ export class ThreeC extends ThreeC_internal {
|
|||||||
dirLight.shadow.camera.near = 0.5;
|
dirLight.shadow.camera.near = 0.5;
|
||||||
dirLight.shadow.camera.far = 200;
|
dirLight.shadow.camera.far = 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
// static setupTopDownLighting() {
|
|
||||||
// let dirLight = this.defaultDirectionalLight;
|
|
||||||
|
|
||||||
// if (!dirLight) {
|
|
||||||
// this.defaultDirectionalLight = new DirectionalLight(0xffffff, 2.5);
|
|
||||||
// dirLight = this.defaultDirectionalLight;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Позиціонуємо світло для топ-даун виду і додаємо в сцену
|
|
||||||
// dirLight.position.set(15, 20, -15);
|
|
||||||
// dirLight.intensity = 3.0;
|
|
||||||
// dirLight.target.position.set(0, 0, 0);
|
|
||||||
// dirLight.castShadow = true;
|
|
||||||
|
|
||||||
// const d = 25;
|
|
||||||
// dirLight.shadow.camera.left = -d;
|
|
||||||
// dirLight.shadow.camera.right = d;
|
|
||||||
// dirLight.shadow.camera.top = d;
|
|
||||||
// dirLight.shadow.camera.bottom = -d;
|
|
||||||
// dirLight.shadow.mapSize.width = 2048;
|
|
||||||
// dirLight.shadow.mapSize.height = 2048;
|
|
||||||
// dirLight.shadow.camera.near = 0.5;
|
|
||||||
// dirLight.shadow.camera.far = 200;
|
|
||||||
|
|
||||||
// this.addToScene(dirLight);
|
|
||||||
|
|
||||||
// // Ensure ambient light exists and is stronger for top-down
|
|
||||||
// if (!this.defaultAmbientLight) {
|
|
||||||
// this.defaultAmbientLight = new AmbientLight(0xffffff, 1.5);
|
|
||||||
// }
|
|
||||||
// this.defaultAmbientLight.intensity = 1.6;
|
|
||||||
// this.addToScene(this.defaultAmbientLight);
|
|
||||||
|
|
||||||
// // Add a soft hemisphere light to fill shadows
|
|
||||||
// const hemi = new HemisphereLight(0xffffbb, 0x080820, 0.6);
|
|
||||||
// this.addToScene(hemi);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export class TimeC {
|
||||||
|
static TimeScale = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { UpdateController } from "@24tools/playable_template";
|
||||||
|
import { UI_IMAGES } from "../../resources/images/uiImages";
|
||||||
|
import { TimeC } from "../Timers/TimeC";
|
||||||
|
|
||||||
|
/** Секунд від 0% до 100% при FILL_SPEED = 1 */
|
||||||
|
const INVASION_PROGRESS_TOTAL_SEC = 120;
|
||||||
|
|
||||||
|
/** Множник швидкості заповнення */
|
||||||
|
const INVASION_PROGRESS_FILL_SPEED = 1;
|
||||||
|
|
||||||
|
export class InvasionProgressUIC {
|
||||||
|
private static fillEl: HTMLElement | null = null;
|
||||||
|
private static progressEl: HTMLElement | null = null;
|
||||||
|
private static elapsedSec = 0;
|
||||||
|
private static isRunning = false;
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
const uiRoot = document.getElementById("ui");
|
||||||
|
if (!uiRoot) return;
|
||||||
|
|
||||||
|
const block = document.createElement("div");
|
||||||
|
block.className = "hud-top-center";
|
||||||
|
|
||||||
|
const title = document.createElement("h1");
|
||||||
|
title.className = "hud-invasion-title";
|
||||||
|
title.textContent = "ZOMBIE INVASION";
|
||||||
|
|
||||||
|
const progress = document.createElement("div");
|
||||||
|
progress.className = "hud-progress";
|
||||||
|
|
||||||
|
const track = document.createElement("div");
|
||||||
|
track.className = "hud-progress__track";
|
||||||
|
|
||||||
|
const fill = document.createElement("div");
|
||||||
|
fill.className = "hud-progress__fill";
|
||||||
|
this.fillEl = fill;
|
||||||
|
|
||||||
|
const icon = document.createElement("img");
|
||||||
|
icon.className = "hud-progress__icon";
|
||||||
|
icon.src = UI_IMAGES.zombieHead;
|
||||||
|
icon.alt = "";
|
||||||
|
|
||||||
|
track.appendChild(fill);
|
||||||
|
progress.appendChild(track);
|
||||||
|
progress.appendChild(icon);
|
||||||
|
this.progressEl = progress;
|
||||||
|
block.appendChild(title);
|
||||||
|
block.appendChild(progress);
|
||||||
|
uiRoot.appendChild(block);
|
||||||
|
|
||||||
|
this.setProgress(0);
|
||||||
|
|
||||||
|
UpdateController.Instance.onUpdate.addDelegate((delta) => this.tick(delta));
|
||||||
|
}
|
||||||
|
|
||||||
|
static startTimer() {
|
||||||
|
this.elapsedSec = 0;
|
||||||
|
this.isRunning = true;
|
||||||
|
this.setProgress(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static tick(delta: number) {
|
||||||
|
if (!this.isRunning || !this.fillEl) return;
|
||||||
|
|
||||||
|
this.elapsedSec +=
|
||||||
|
delta * TimeC.TimeScale * INVASION_PROGRESS_FILL_SPEED;
|
||||||
|
|
||||||
|
const ratio = Math.min(
|
||||||
|
1,
|
||||||
|
this.elapsedSec / INVASION_PROGRESS_TOTAL_SEC,
|
||||||
|
);
|
||||||
|
this.setProgress(ratio);
|
||||||
|
|
||||||
|
if (ratio >= 1) {
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static setProgress(ratio: number) {
|
||||||
|
if (!this.fillEl) return;
|
||||||
|
|
||||||
|
const clamped = Math.max(0, Math.min(1, ratio));
|
||||||
|
this.fillEl.style.width = `${clamped * 100}%`;
|
||||||
|
this.progressEl?.style.setProperty("--progress", String(clamped));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,18 +95,18 @@ canvas {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#editor {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ui,
|
|
||||||
#editor {
|
#ui {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: calc(100vh * 9 / 16);
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
pointer-events: none;
|
||||||
|
padding: 2vw;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#modal {
|
#modal {
|
||||||
|
|||||||
@@ -1,37 +1,176 @@
|
|||||||
.resource-bar {
|
.hud-top-center {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 2vh;
|
top: 2vh;
|
||||||
left: 12vw;
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1vh;
|
align-items: center;
|
||||||
|
gap: 2vh;
|
||||||
|
overflow: visible;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hud-invasion-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 3.2vh;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
-webkit-text-stroke: 0.12vh #000000;
|
||||||
|
paint-order: stroke fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-progress {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 3.2vh;
|
||||||
|
max-width: 55vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-progress__track {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0.35vh solid #000000;
|
||||||
|
border-radius: 0.5vh;
|
||||||
|
background: linear-gradient(180deg, #5fe86a 0%, #2db83a 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-progress__fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 0.15vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-progress__icon {
|
||||||
|
position: absolute;
|
||||||
|
left: clamp(
|
||||||
|
2.75vh,
|
||||||
|
calc(var(--progress, 0) * 100%),
|
||||||
|
calc(100% - 2.75vh)
|
||||||
|
);
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
height: 5.5vh;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-top-right {
|
||||||
|
position: absolute;
|
||||||
|
top: 5.5vh;
|
||||||
|
right: 2vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 3vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-avatar {
|
||||||
|
width: 8vh;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 1vh;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.resource-counter {
|
.resource-counter {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1vw;
|
justify-content: flex-start;
|
||||||
padding: 0.6vh 1.2vw;
|
width: 12vh;
|
||||||
background: rgba(0, 0, 0, 0.45);
|
height: 4vh;
|
||||||
border-radius: 0.8vh;
|
padding: 0 9vh 0 3vh;
|
||||||
|
background-color: transparent;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-icon {
|
.resource-icon {
|
||||||
width: 4vh;
|
position: absolute;
|
||||||
height: 4vh;
|
right: 1.2vh;
|
||||||
border-radius: 0.5vh;
|
top: 50%;
|
||||||
border: 0.2vh solid rgba(255, 255, 255, 0.35);
|
transform: translateY(-50%);
|
||||||
|
width: 4.5vh;
|
||||||
|
height: 4.5vh;
|
||||||
|
border-radius: 0.4vh;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-count {
|
.resource-count {
|
||||||
min-width: 2ch;
|
min-width: 2ch;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 3vh;
|
font-size: 2.4vh;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1;
|
||||||
|
-webkit-text-stroke: 0.15vh #000000;
|
||||||
|
paint-order: stroke fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-bottom-left {
|
||||||
|
position: absolute;
|
||||||
|
left: 2vw;
|
||||||
|
bottom: 30vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-weapon {
|
||||||
|
position: relative;
|
||||||
|
width: 9vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-weapon__bg {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-weapon__tool {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 38%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 58%;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-weapon__level {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 14%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.8vh;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-text-stroke: 0.12vh #000000;
|
||||||
|
paint-order: stroke fill;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-fly-pickup {
|
.resource-fly-pickup {
|
||||||
@@ -44,3 +183,115 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
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) {
|
||||||
|
.hud-top-center {
|
||||||
|
top: 1.2vh;
|
||||||
|
width: 55%;
|
||||||
|
max-width: 36vh;
|
||||||
|
gap: 0.5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-invasion-title {
|
||||||
|
font-size: 2.6vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-progress {
|
||||||
|
height: 2.6vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-progress__icon {
|
||||||
|
height: 4.2vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-top-right {
|
||||||
|
top: 1vh;
|
||||||
|
right: 1.5vw;
|
||||||
|
gap: 0.5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-avatar {
|
||||||
|
width: 10vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-counter {
|
||||||
|
width: 12vh;
|
||||||
|
height: 4vh;
|
||||||
|
padding: 0 7.5vh 0 2.5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-icon {
|
||||||
|
width: 3.8vh;
|
||||||
|
height: 3.8vh;
|
||||||
|
right: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-count {
|
||||||
|
font-size: 2.2vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-bottom-left {
|
||||||
|
left: 20px;
|
||||||
|
bottom: 22vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-weapon {
|
||||||
|
width: 16vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-weapon__level {
|
||||||
|
font-size: 1.5vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
import { FontFamily, formFontFamily } from "@24tools/ads_common";
|
import { FontFamily, formFontFamily } from "@24tools/ads_common";
|
||||||
|
|
||||||
export const customFont: undefined | Promise<FontFamily> = undefined
|
export const customFont: Promise<FontFamily> = formFontFamily(
|
||||||
|
"Passion One",
|
||||||
|
"./PassionOne-Black.otf"
|
||||||
|
);
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
|
|
||||||
<div id="constructor">
|
<div id="constructor">
|
||||||
<div id="ui"></div>
|
<div id="ui"></div>
|
||||||
<div id="editor"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<script id="dev-start">
|
<script id="dev-start">
|
||||||
window.onload = function () {
|
window.onload = function () {
|
||||||
|
|||||||
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 814 B |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
import woodCounterBg from "./ResourceBackground_Wood.webp";
|
||||||
|
import avatarTop from "./ZombiePunk_Icon-Top.webp";
|
||||||
|
import toolBg from "./Tool_Backgtound.webp";
|
||||||
|
import toolBat from "./Tool_2.webp";
|
||||||
|
import zombieHead from "./Icon_Zombie_Head.webp";
|
||||||
|
|
||||||
|
export const UI_IMAGES = {
|
||||||
|
woodCounterBg,
|
||||||
|
avatarTop,
|
||||||
|
toolBg,
|
||||||
|
toolBat,
|
||||||
|
zombieHead,
|
||||||
|
} as const;
|
||||||
@@ -13,6 +13,10 @@ export const meshes : ConvertResourceType = {
|
|||||||
name: "character",
|
name: "character",
|
||||||
value: ConvertToBase64WhenRelease("./ZombiePunk_Character.glb"),
|
value: ConvertToBase64WhenRelease("./ZombiePunk_Character.glb"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "wood",
|
||||||
|
value: ConvertToBase64WhenRelease("./wood.glb"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
loader: Template3d.meshLoader
|
loader: Template3d.meshLoader
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ConvertResourcesType } from "@24tools/playable_template";
|
import { ConvertResourcesType } from "@24tools/playable_template";
|
||||||
import { meshes } from "./meshes/meshes";
|
import { meshes } from "./meshes/meshes";
|
||||||
import { sounds } from "./sounds/sounds";
|
import { sounds } from "./sounds/sounds";
|
||||||
|
import { vfx } from "./vfx/vfx";
|
||||||
|
|
||||||
export const resources: ConvertResourcesType = [meshes, sounds];
|
export const resources: ConvertResourcesType = [meshes, sounds, vfx];
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { ConvertToBase64WhenRelease } from "@24tools/ads_common";
|
||||||
|
import { ConvertResourceType, quarksLoader } from "@24tools/playable_template";
|
||||||
|
|
||||||
|
export enum VfxType {
|
||||||
|
LootableHit = "lootable_hit",
|
||||||
|
LootableDestroy = "lootable_destroy",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const vfx: ConvertResourceType = {
|
||||||
|
type: "vfx",
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
name: VfxType.LootableHit,
|
||||||
|
value: ConvertToBase64WhenRelease("./VFX_Lootable_Hit.json"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: VfxType.LootableDestroy,
|
||||||
|
value: ConvertToBase64WhenRelease("./VFX_Lootable_Destroy.json"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
loader: quarksLoader,
|
||||||
|
};
|
||||||
@@ -24,7 +24,7 @@ export const beforeResourcesLoadedCb = () => {
|
|||||||
ThreeC_internal.init();
|
ThreeC_internal.init();
|
||||||
ThreeC.createBaseLights();
|
ThreeC.createBaseLights();
|
||||||
ThreeC.setupDirectionalLight();
|
ThreeC.setupDirectionalLight();
|
||||||
PlayerInput.InitJoystick();
|
PlayerInput.initJoystick();
|
||||||
|
|
||||||
|
|
||||||
let physicsWorld = Physics_internal.init(new Vec3(0, 0, 0));
|
let physicsWorld = Physics_internal.init(new Vec3(0, 0, 0));
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { InvasionProgressUIC } from "../controllers/UI/InvasionProgressUIC";
|
||||||
|
|
||||||
export const firstClickCb: () => void = () => {
|
export const firstClickCb: () => void = () => {
|
||||||
console.log("First click");
|
InvasionProgressUIC.startTimer();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Template3d } from "@24tools/playable_template";
|
import { Template3d } from "@24tools/playable_template";
|
||||||
|
import { CameraC } from "../controllers/CameraC";
|
||||||
|
|
||||||
export const resizeCb = () => {
|
export const resizeCb = () => {
|
||||||
Template3d.resize();
|
Template3d.resize();
|
||||||
|
CameraC.setCamera(window.screenSize.portrait);
|
||||||
};
|
};
|
||||||
|
|||||||