day 5
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
import { JoystickC, UpdateController } from "@24tools/playable_template";
|
||||||
|
import { Vector3 } from "three";
|
||||||
|
import { PhysicsTriggerC } from "./PhysicsTriggerC";
|
||||||
|
import { PropC, PropRegistry } from "./PropC";
|
||||||
|
import { Player } from "../Presets/Player";
|
||||||
|
|
||||||
|
export class GatherC {
|
||||||
|
private static activeProps = new Set<PropC>();
|
||||||
|
private static pendingAutoAttack = false;
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
PhysicsTriggerC.onTriggerEnter.addDelegate((payload) => {
|
||||||
|
const prop = PropRegistry.get(payload.lootableObject);
|
||||||
|
if (!prop || prop.isBroken) return;
|
||||||
|
|
||||||
|
this.activeProps.add(prop);
|
||||||
|
this.scheduleAutoAttackCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
PhysicsTriggerC.onTriggerExit.addDelegate((payload) => {
|
||||||
|
const prop = PropRegistry.get(payload.lootableObject);
|
||||||
|
if (prop) this.activeProps.delete(prop);
|
||||||
|
|
||||||
|
this.removeBrokenProps();
|
||||||
|
|
||||||
|
if (this.getAttackableProps().length === 0) {
|
||||||
|
Player.stopAutoAttack();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JoystickC.onJoysticEnd.addDelegate(() => {
|
||||||
|
this.scheduleAutoAttackCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
UpdateController.Instance.onUpdate.addDelegate(() => {
|
||||||
|
if (!this.pendingAutoAttack) return;
|
||||||
|
this.pendingAutoAttack = false;
|
||||||
|
this.tryStartAutoAttack();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static scheduleAutoAttackCheck() {
|
||||||
|
this.pendingAutoAttack = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static onAttackHit() {
|
||||||
|
for (const prop of this.getAttackableProps()) {
|
||||||
|
prop.takeDamage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeBrokenProps();
|
||||||
|
|
||||||
|
if (this.getAttackableProps().length === 0) {
|
||||||
|
Player.stopAutoAttack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player.playAttack(() => this.onAttackHit());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static tryStartAutoAttack() {
|
||||||
|
const props = this.getAttackableProps();
|
||||||
|
if (props.length === 0) return;
|
||||||
|
if (Player.isMoving()) return;
|
||||||
|
|
||||||
|
const targetPosition = this.getTargetPosition(props);
|
||||||
|
Player.startAutoAttack(() => this.onAttackHit(), targetPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getTargetPosition(props: PropC[]): Vector3 {
|
||||||
|
const playerPosition = Player.getWorldPosition();
|
||||||
|
const target = new Vector3();
|
||||||
|
let closestDistanceSq = Infinity;
|
||||||
|
|
||||||
|
for (const prop of props) {
|
||||||
|
const propPosition = prop.object.getWorldPosition(new Vector3());
|
||||||
|
const distanceSq = playerPosition.distanceToSquared(propPosition);
|
||||||
|
if (distanceSq >= closestDistanceSq) continue;
|
||||||
|
|
||||||
|
closestDistanceSq = distanceSq;
|
||||||
|
target.copy(propPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getAttackableProps() {
|
||||||
|
return [...this.activeProps].filter((prop) => !prop.isBroken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static removeBrokenProps() {
|
||||||
|
for (const prop of this.activeProps) {
|
||||||
|
if (prop.isBroken) this.activeProps.delete(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
-9
@@ -1,6 +1,12 @@
|
|||||||
import { Object3D } from "three";
|
import { Box3, BoxGeometry, Mesh, Object3D, Vector3 } from "three";
|
||||||
import { ThreeC } from "../ThreeC";
|
import { ThreeC } from "../ThreeC";
|
||||||
import { PhysicsBody, PhysicsLayer } from "../PhysicsC";
|
import { PhysicsBody, PhysicsLayer } from "../PhysicsC";
|
||||||
|
import { PhysicsTriggerC } from "./PhysicsTriggerC";
|
||||||
|
import { PropC } from "./PropC";
|
||||||
|
import { resolvePropType } from "../Resources/PropDropTable";
|
||||||
|
|
||||||
|
const MAP_PHYSICS_LAYERS = ["Colliders", "Lootable"];
|
||||||
|
const GATHER_TRIGGER_PADDING = 0.2;
|
||||||
|
|
||||||
export class Map {
|
export class Map {
|
||||||
static Init() {
|
static Init() {
|
||||||
@@ -14,19 +20,109 @@ export class Map {
|
|||||||
ThreeC.setShadowsStateForChildren(mapObject, true, true);
|
ThreeC.setShadowsStateForChildren(mapObject, true, true);
|
||||||
ThreeC.addToScene(mapObject);
|
ThreeC.addToScene(mapObject);
|
||||||
|
|
||||||
const colliders = mapObject.getObjectByName("Colliders");
|
PhysicsTriggerC.init();
|
||||||
if (colliders) {
|
this.buildPhysics(mapObject);
|
||||||
colliders.visible = false;
|
}
|
||||||
this.buildColliders(colliders);
|
|
||||||
|
private static buildPhysics(mapObject: Object3D) {
|
||||||
|
for (const layerName of MAP_PHYSICS_LAYERS) {
|
||||||
|
const layer = mapObject.getObjectByName(layerName);
|
||||||
|
if (!layer) {
|
||||||
|
console.warn(`Map physics layer not found: ${layerName}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layerName === "Colliders") {
|
||||||
|
layer.visible = false;
|
||||||
|
this.addWallPhysics(layer);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.children.forEach((lootableObject) => {
|
||||||
|
this.setupLootableObject(lootableObject);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static buildColliders(collidersRoot: Object3D) {
|
private static addWallPhysics(root: Object3D) {
|
||||||
collidersRoot.traverse((child) => {
|
root.traverse((child) => {
|
||||||
if (child === collidersRoot) return;
|
if (child === root) return;
|
||||||
if ((child as any).isMesh) {
|
if ((child as Mesh).isMesh) {
|
||||||
new PhysicsBody(child, false, 0, PhysicsLayer.Wall, PhysicsLayer.Player);
|
new PhysicsBody(child, false, 0, PhysicsLayer.Wall, PhysicsLayer.Player);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static setupLootableObject(lootableObject: Object3D) {
|
||||||
|
const propType = resolvePropType(lootableObject.name);
|
||||||
|
const prop = new PropC(lootableObject, propType);
|
||||||
|
const colliderMesh = this.findMesh(lootableObject, (name) => name.includes("collider"));
|
||||||
|
const visualMeshes = this.getVisualMeshes(lootableObject);
|
||||||
|
|
||||||
|
if (colliderMesh) {
|
||||||
|
colliderMesh.visible = false;
|
||||||
|
prop.addWallBody(
|
||||||
|
new PhysicsBody(colliderMesh, false, 0, PhysicsLayer.Wall, PhysicsLayer.Player),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
visualMeshes.forEach((mesh) => {
|
||||||
|
prop.addWallBody(
|
||||||
|
new PhysicsBody(mesh, false, 0, PhysicsLayer.Wall, PhysicsLayer.Player),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
PhysicsTriggerC.register(this.createGatherTrigger(lootableObject), lootableObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static findMesh(
|
||||||
|
root: Object3D,
|
||||||
|
matcher: (name: string) => boolean,
|
||||||
|
): Mesh | null {
|
||||||
|
let found: Mesh | null = null;
|
||||||
|
|
||||||
|
root.traverse((child) => {
|
||||||
|
if (found) return;
|
||||||
|
if ((child as Mesh).isMesh && matcher(child.name.toLowerCase())) {
|
||||||
|
found = child as Mesh;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getVisualMeshes(root: Object3D): Mesh[] {
|
||||||
|
const visualMeshes: Mesh[] = [];
|
||||||
|
|
||||||
|
root.traverse((child) => {
|
||||||
|
if (!(child as Mesh).isMesh) return;
|
||||||
|
|
||||||
|
const name = child.name.toLowerCase();
|
||||||
|
if (name.includes("collider") || name.includes("trigger") || name.includes("gathertrigger")) return;
|
||||||
|
|
||||||
|
visualMeshes.push(child as Mesh);
|
||||||
|
});
|
||||||
|
|
||||||
|
return visualMeshes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createGatherTrigger(lootableObject: Object3D): Mesh {
|
||||||
|
lootableObject.updateWorldMatrix(true, true);
|
||||||
|
|
||||||
|
const size = new Vector3();
|
||||||
|
new Box3().setFromObject(lootableObject).getSize(size);
|
||||||
|
|
||||||
|
const triggerMesh = new Mesh(
|
||||||
|
new BoxGeometry(
|
||||||
|
size.x + GATHER_TRIGGER_PADDING * 2,
|
||||||
|
size.y + GATHER_TRIGGER_PADDING * 2,
|
||||||
|
size.z + GATHER_TRIGGER_PADDING * 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
triggerMesh.visible = false;
|
||||||
|
triggerMesh.name = "GatherTrigger";
|
||||||
|
lootableObject.add(triggerMesh);
|
||||||
|
|
||||||
|
return triggerMesh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import {
|
||||||
|
EasyEvent,
|
||||||
|
Physics_internal,
|
||||||
|
UpdateController,
|
||||||
|
} from "@24tools/playable_template";
|
||||||
|
import { Object3D } from "three";
|
||||||
|
import { Body, Box } from "cannon-es";
|
||||||
|
import { PhysicsBody, PhysicsLayer } from "../PhysicsC";
|
||||||
|
|
||||||
|
const PLAYER_RADIUS = 0.3;
|
||||||
|
|
||||||
|
export type TriggerEventPayload = {
|
||||||
|
lootableObject: Object3D;
|
||||||
|
triggerObject: Object3D;
|
||||||
|
playerBody: Body;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TriggerRecord = {
|
||||||
|
lootableObject: Object3D;
|
||||||
|
triggerObject: Object3D;
|
||||||
|
radius: number;
|
||||||
|
position: Body["position"];
|
||||||
|
physicsBody: PhysicsBody;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PhysicsTriggerC {
|
||||||
|
private static inited = false;
|
||||||
|
private static triggers: TriggerRecord[] = [];
|
||||||
|
private static activeTriggers = new Set<TriggerRecord>();
|
||||||
|
|
||||||
|
static onTriggerEnter = new EasyEvent<TriggerEventPayload>();
|
||||||
|
static onTriggerExit = new EasyEvent<TriggerEventPayload>();
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
if (this.inited) return;
|
||||||
|
this.inited = true;
|
||||||
|
|
||||||
|
UpdateController.Instance.onUpdate.addDelegate(() => this.update());
|
||||||
|
}
|
||||||
|
|
||||||
|
static register(triggerObject: Object3D, lootableObject: Object3D) {
|
||||||
|
const physicsBody = new PhysicsBody(
|
||||||
|
triggerObject,
|
||||||
|
true,
|
||||||
|
0,
|
||||||
|
PhysicsLayer.Trigger,
|
||||||
|
PhysicsLayer.Player,
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = physicsBody.getPhysicsBody();
|
||||||
|
body.allowSleep = false;
|
||||||
|
|
||||||
|
const shape = body.shapes[0] as Box;
|
||||||
|
const halfExtents = shape.halfExtents;
|
||||||
|
|
||||||
|
this.triggers.push({
|
||||||
|
lootableObject,
|
||||||
|
triggerObject,
|
||||||
|
radius: Math.max(halfExtents.x, halfExtents.y, halfExtents.z),
|
||||||
|
position: body.position,
|
||||||
|
physicsBody,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static unregister(lootableObject: Object3D) {
|
||||||
|
this.triggers = this.triggers.filter((record) => {
|
||||||
|
if (record.lootableObject !== lootableObject) return true;
|
||||||
|
|
||||||
|
record.physicsBody.destroy();
|
||||||
|
this.activeTriggers.delete(record);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static update() {
|
||||||
|
const playerBody = Physics_internal.physicsWorld?.bodies.find(
|
||||||
|
(body) => body.collisionFilterGroup === PhysicsLayer.Player,
|
||||||
|
);
|
||||||
|
if (!playerBody) return;
|
||||||
|
|
||||||
|
for (const trigger of this.triggers) {
|
||||||
|
const dx = playerBody.position.x - trigger.position.x;
|
||||||
|
const dy = playerBody.position.y - trigger.position.y;
|
||||||
|
const dz = playerBody.position.z - trigger.position.z;
|
||||||
|
const distanceSq = dx * dx + dy * dy + dz * dz;
|
||||||
|
const isInside = distanceSq <= (trigger.radius + PLAYER_RADIUS) ** 2;
|
||||||
|
const wasInside = this.activeTriggers.has(trigger);
|
||||||
|
|
||||||
|
if (isInside === wasInside) continue;
|
||||||
|
|
||||||
|
const payload: TriggerEventPayload = {
|
||||||
|
lootableObject: trigger.lootableObject,
|
||||||
|
triggerObject: trigger.triggerObject,
|
||||||
|
playerBody,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isInside) {
|
||||||
|
this.activeTriggers.add(trigger);
|
||||||
|
this.onTriggerEnter.Invoke(payload);
|
||||||
|
console.log("[Gather Enter]", trigger.lootableObject.name);
|
||||||
|
} else {
|
||||||
|
this.activeTriggers.delete(trigger);
|
||||||
|
this.onTriggerExit.Invoke(payload);
|
||||||
|
console.log("[Gather Exit]", trigger.lootableObject.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { Object3D } from "three";
|
||||||
|
import { PhysicsBody } from "../PhysicsC";
|
||||||
|
import { PropType } from "./PropType";
|
||||||
|
import { PhysicsTriggerC } from "./PhysicsTriggerC";
|
||||||
|
import { PROP_DROPS } from "../Resources/PropDropTable";
|
||||||
|
import { createDropPlan, PropDropPlan } from "../Resources/PropDropPlanner";
|
||||||
|
import { ResourceSpawnC } from "../Resources/ResourceSpawnC";
|
||||||
|
|
||||||
|
export class PropRegistry {
|
||||||
|
private static props = new Map<Object3D, PropC>();
|
||||||
|
|
||||||
|
static register(prop: PropC) {
|
||||||
|
this.props.set(prop.object, prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get(object: Object3D) {
|
||||||
|
return this.props.get(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PropC {
|
||||||
|
readonly object: Object3D;
|
||||||
|
readonly propType: PropType;
|
||||||
|
health: number;
|
||||||
|
readonly maxHealth: number;
|
||||||
|
isBroken = false;
|
||||||
|
|
||||||
|
private wallBodies: PhysicsBody[] = [];
|
||||||
|
private dropPlan: PropDropPlan | null = null;
|
||||||
|
|
||||||
|
constructor(object: Object3D, propType: PropType, maxHealth = 3) {
|
||||||
|
this.object = object;
|
||||||
|
this.propType = propType;
|
||||||
|
this.maxHealth = maxHealth;
|
||||||
|
this.health = maxHealth;
|
||||||
|
PropRegistry.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
addWallBody(physicsBody: PhysicsBody) {
|
||||||
|
this.wallBodies.push(physicsBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
takeDamage(amount: number): boolean {
|
||||||
|
if (this.isBroken) return false;
|
||||||
|
|
||||||
|
const hitIndex = this.maxHealth - this.health;
|
||||||
|
this.ensureDropPlan();
|
||||||
|
|
||||||
|
this.health -= amount;
|
||||||
|
this.spawnForHit(hitIndex);
|
||||||
|
|
||||||
|
if (this.health > 0) return false;
|
||||||
|
|
||||||
|
this.break();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break() {
|
||||||
|
if (this.isBroken) return;
|
||||||
|
|
||||||
|
this.isBroken = true;
|
||||||
|
this.object.visible = false;
|
||||||
|
|
||||||
|
this.wallBodies.forEach((body) => body.destroy());
|
||||||
|
this.wallBodies = [];
|
||||||
|
|
||||||
|
PhysicsTriggerC.unregister(this.object);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureDropPlan() {
|
||||||
|
if (this.dropPlan) return;
|
||||||
|
|
||||||
|
const rules = PROP_DROPS[this.propType];
|
||||||
|
if (!rules.length) return;
|
||||||
|
|
||||||
|
this.dropPlan = createDropPlan(this.maxHealth, rules[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private spawnForHit(hitIndex: number) {
|
||||||
|
if (!this.dropPlan) return;
|
||||||
|
|
||||||
|
let spawnCount = this.dropPlan.hitAmounts[hitIndex] ?? 0;
|
||||||
|
|
||||||
|
if (this.health <= 0) {
|
||||||
|
const remaining = this.dropPlan.totalDrop - this.dropPlan.spawnedDrop - spawnCount;
|
||||||
|
if (remaining > 0) {
|
||||||
|
spawnCount += remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spawnCount <= 0) return;
|
||||||
|
|
||||||
|
ResourceSpawnC.spawnFromProp(this, this.dropPlan.resource, spawnCount);
|
||||||
|
this.dropPlan.spawnedDrop += spawnCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export enum PropType {
|
||||||
|
Box = "box",
|
||||||
|
}
|
||||||
@@ -51,6 +51,26 @@ export class Character {
|
|||||||
this.curClipAction.weight = weight;
|
this.curClipAction.weight = weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setObjectVisible(name: string, visible: boolean) {
|
||||||
|
this.tObj.traverse((child) => {
|
||||||
|
if (child.name === name) {
|
||||||
|
child.visible = visible;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaultWeapons() {
|
||||||
|
this.setObjectVisible("Tool_1", false);
|
||||||
|
this.setObjectVisible("Tool_2", true);
|
||||||
|
this.setObjectVisible("Character_Pistol", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBatEquipped(equipped: boolean) {
|
||||||
|
this.setObjectVisible("Tool_1", equipped);
|
||||||
|
this.setObjectVisible("Tool_2", !equipped);
|
||||||
|
this.setObjectVisible("Character_Pistol", false);
|
||||||
|
}
|
||||||
|
|
||||||
playAnimation(anim_id: number, one_time: boolean = false, fade = 0.25, randomStart = false) {
|
playAnimation(anim_id: number, one_time: boolean = false, fade = 0.25, randomStart = false) {
|
||||||
let oldClipAction: null | AnimationAction = this.curClipAction;
|
let oldClipAction: null | AnimationAction = this.curClipAction;
|
||||||
var clipAction = this.animMixer.clipAction(this.animationList[anim_id]);
|
var clipAction = this.animMixer.clipAction(this.animationList[anim_id]);
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
export enum BaseAnimation {
|
export enum BaseAnimation {
|
||||||
Nan = -1,
|
|
||||||
Idle = 0,
|
Nan = -1,
|
||||||
Run = 1,
|
|
||||||
}
|
Idle = 0,
|
||||||
|
|
||||||
|
Run = 1,
|
||||||
|
|
||||||
|
WeaponTakeOut = 2,
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export class PlayerInput implements IMoveInput {
|
|||||||
private static threshold = 0.25;
|
private static threshold = 0.25;
|
||||||
|
|
||||||
get CurrentDirection() { return this.currentDirection.clone(); };
|
get CurrentDirection() { return this.currentDirection.clone(); };
|
||||||
|
get IsActive() { return this.inputaActive; };
|
||||||
|
|
||||||
inputaActive: boolean = false;
|
inputaActive: boolean = false;
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ export class RotationC {
|
|||||||
|
|
||||||
private update(delta: number) {
|
private update(delta: number) {
|
||||||
const loockAtStep = this.input.CurrentDirection;
|
const loockAtStep = this.input.CurrentDirection;
|
||||||
if (loockAtStep.length() != 0) {
|
if (loockAtStep.length() == 0) return;
|
||||||
const loockAtPoint = this.target.position.clone().add(loockAtStep);
|
|
||||||
this.target.lookAt(loockAtPoint);
|
this.currentQ.copy(this.target.quaternion);
|
||||||
this.targetQ.copy(this.target.quaternion);
|
|
||||||
this.target.quaternion.copy(this.currentQ);
|
const loockAtPoint = this.target.position.clone().add(loockAtStep);
|
||||||
}
|
this.target.lookAt(loockAtPoint);
|
||||||
|
this.targetQ.copy(this.target.quaternion);
|
||||||
|
this.target.quaternion.copy(this.currentQ);
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -5,22 +5,27 @@ 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 } from "three";
|
import { Object3D, Quaternion, 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 { Vec3 } from "cannon-es";
|
|
||||||
import { contain } from "three/src/extras/TextureUtils";
|
|
||||||
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 { Vector3 } from "three";
|
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
private static inited: boolean = false;
|
private static inited: boolean = false;
|
||||||
private static isRunning: boolean = false;
|
private static isRunning: boolean = 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;
|
||||||
@@ -28,6 +33,9 @@ export class Player {
|
|||||||
private static rotation: RotationC;
|
private static rotation: RotationC;
|
||||||
private static spawnPosition: Vector3 = new Vector3(0, 0, 0);
|
private static spawnPosition: Vector3 = new Vector3(0, 0, 0);
|
||||||
|
|
||||||
|
private static readonly turnSpeed = 8;
|
||||||
|
private static readonly turnCompleteAngle = 0.05;
|
||||||
|
|
||||||
static character: Character;
|
static character: Character;
|
||||||
static physics: PhysicsBody;
|
static physics: PhysicsBody;
|
||||||
|
|
||||||
@@ -37,21 +45,24 @@ export class Player {
|
|||||||
|
|
||||||
static Init() {
|
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);
|
||||||
|
|
||||||
this.container.position.copy(this.spawnPosition);
|
this.container.position.copy(this.spawnPosition);
|
||||||
this.character.playAnimation(BaseAnimation.Idle)
|
this.character.setDefaultWeapons();
|
||||||
|
this.character.playAnimation(BaseAnimation.Idle);
|
||||||
this.container.add(this.character.tObj);
|
this.container.add(this.character.tObj);
|
||||||
ThreeC.addToScene(this.container);
|
ThreeC.addToScene(this.container);
|
||||||
|
|
||||||
this.InitPhisic();
|
this.InitPhisic();
|
||||||
const input = new PlayerInput();
|
this.input = new PlayerInput();
|
||||||
const moveSpeed = 3;
|
const moveSpeed = 3;
|
||||||
const rotationSpeed = 8;
|
const rotationSpeed = 8;
|
||||||
this.movement = new MoveC(input, moveSpeed);
|
this.movement = new MoveC(this.input, moveSpeed);
|
||||||
this.rotation = new RotationC(this.container, input, rotationSpeed);
|
this.rotation = new RotationC(this.container, this.input, rotationSpeed);
|
||||||
|
|
||||||
|
this.character.onAnimFinish.addDelegate(() => this.OnCombatAnimationFinished());
|
||||||
|
|
||||||
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);
|
||||||
@@ -59,23 +70,136 @@ export class Player {
|
|||||||
FollowCameraC.Init(this.container);
|
FollowCameraC.Init(this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getWorldPosition() {
|
||||||
|
return this.container.getWorldPosition(new Vector3());
|
||||||
|
}
|
||||||
|
|
||||||
|
static isMoving() {
|
||||||
|
return this.input.IsActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
static startAutoAttack(onHit: () => void, targetWorldPosition: Vector3) {
|
||||||
|
if (this.isAutoAttacking) return;
|
||||||
|
|
||||||
|
this.isAutoAttacking = true;
|
||||||
|
this.beginTurnToTarget(targetWorldPosition, () => this.playAttack(onHit));
|
||||||
|
}
|
||||||
|
|
||||||
|
static stopAutoAttack() {
|
||||||
|
this.isAutoAttacking = false;
|
||||||
|
this.onAttackComplete = null;
|
||||||
|
this.cancelTurnToTarget();
|
||||||
|
this.resetCombatState();
|
||||||
|
|
||||||
|
if (!this.isAttacking) {
|
||||||
|
this.StopRunning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static playAttack(onComplete: () => void) {
|
||||||
|
this.onAttackComplete = onComplete;
|
||||||
|
this.isAttacking = true;
|
||||||
|
this.isRunning = false;
|
||||||
|
|
||||||
|
this.physics.getPhysicsBody().velocity.set(0, 0, 0);
|
||||||
|
|
||||||
|
if (!this.isBatEquipped) {
|
||||||
|
this.equipBat();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playBatAttack();
|
||||||
|
}
|
||||||
|
|
||||||
private static InitPhisic() {
|
private static InitPhisic() {
|
||||||
this.physics = new PhysicsBody(
|
this.physics = new PhysicsBody(
|
||||||
this.container,
|
this.container,
|
||||||
false,
|
false,
|
||||||
1,
|
1,
|
||||||
PhysicsLayer.Player,
|
PhysicsLayer.Player,
|
||||||
PhysicsLayer.Wall,
|
PhysicsLayer.Wall | PhysicsLayer.Trigger,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static beginTurnToTarget(targetWorldPosition: Vector3, onComplete: () => void) {
|
||||||
|
const worldPosition = this.container.getWorldPosition(new Vector3());
|
||||||
|
const lookAtPoint = targetWorldPosition.clone();
|
||||||
|
lookAtPoint.y = worldPosition.y;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
private static StartRunning() {
|
||||||
if (this.isRunning) return;
|
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 StopRunning() {
|
||||||
if (!this.isRunning) return;
|
if (!this.isRunning || this.isAttacking || this.isTurningToTarget) return;
|
||||||
this.character.playAnimation(BaseAnimation.Idle);
|
this.character.playAnimation(BaseAnimation.Idle);
|
||||||
this.AnimationValue = 1;
|
this.AnimationValue = 1;
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
@@ -87,6 +211,23 @@ export class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Update(delta: number) {
|
private static Update(delta: number) {
|
||||||
|
if (this.input.IsActive && (this.isAutoAttacking || this.isTurningToTarget)) {
|
||||||
|
this.stopAutoAttack();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isTurningToTarget) {
|
||||||
|
this.physics.getPhysicsBody().velocity.set(0, 0, 0);
|
||||||
|
this.updateTurnToTarget(delta);
|
||||||
|
this.MoveVisual(delta);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAttacking) {
|
||||||
|
this.physics.getPhysicsBody().velocity.set(0, 0, 0);
|
||||||
|
this.MoveVisual(delta);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const diraction = this.movement.Diraction;
|
const diraction = this.movement.Diraction;
|
||||||
const weight = this.movement.Weight;
|
const weight = this.movement.Weight;
|
||||||
|
|
||||||
@@ -98,20 +239,17 @@ export class Player {
|
|||||||
this.StopRunning();
|
this.StopRunning();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const cPos = Vector3TToC(diraction);
|
const cPos = Vector3TToC(diraction);
|
||||||
|
|
||||||
this.physics.getPhysicsBody().velocity.copy(cPos);
|
this.physics.getPhysicsBody().velocity.copy(cPos);
|
||||||
|
this.physics.getPhysicsBody().wakeUp();
|
||||||
this.MoveVisual(delta);
|
this.MoveVisual(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MoveVisual(delta: number) {
|
private static MoveVisual(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,65 @@
|
|||||||
|
function randomInt(min: number, max: number) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shuffle<T>(items: T[]) {
|
||||||
|
const result = [...items];
|
||||||
|
for (let i = result.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[result[i], result[j]] = [result[j], result[i]];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitRandom(total: number, parts: number) {
|
||||||
|
if (parts <= 0) return [];
|
||||||
|
if (parts === 1) return [total];
|
||||||
|
|
||||||
|
const result = new Array(parts).fill(1);
|
||||||
|
let remaining = total - parts;
|
||||||
|
|
||||||
|
while (remaining > 0) {
|
||||||
|
result[Math.floor(Math.random() * parts)]++;
|
||||||
|
remaining--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
import { ResourceType } from "./ResourceType";
|
||||||
|
|
||||||
|
export type PropDropPlan = {
|
||||||
|
resource: ResourceType;
|
||||||
|
totalDrop: number;
|
||||||
|
spawnedDrop: number;
|
||||||
|
hitAmounts: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createDropPlan(
|
||||||
|
maxHealth: number,
|
||||||
|
rule: {
|
||||||
|
resource: ResourceType;
|
||||||
|
minTotal: number;
|
||||||
|
maxTotal: number;
|
||||||
|
minSpawnHits: number;
|
||||||
|
maxSpawnHits: number;
|
||||||
|
},
|
||||||
|
): PropDropPlan {
|
||||||
|
const totalDrop = randomInt(rule.minTotal, rule.maxTotal);
|
||||||
|
const spawnEvents = randomInt(rule.minSpawnHits, rule.maxSpawnHits);
|
||||||
|
const hitAmounts = new Array(maxHealth).fill(0);
|
||||||
|
|
||||||
|
const hitIndexes = shuffle([...Array(maxHealth).keys()]).slice(0, spawnEvents);
|
||||||
|
const amounts = splitRandom(totalDrop, spawnEvents);
|
||||||
|
|
||||||
|
hitIndexes.forEach((hitIndex, index) => {
|
||||||
|
hitAmounts[hitIndex] = amounts[index];
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
resource: rule.resource,
|
||||||
|
totalDrop,
|
||||||
|
spawnedDrop: 0,
|
||||||
|
hitAmounts,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { PropType } from "../Map/PropType";
|
||||||
|
import { ResourceType } from "./ResourceType";
|
||||||
|
|
||||||
|
export type DropRule = {
|
||||||
|
resource: ResourceType;
|
||||||
|
minTotal: number;
|
||||||
|
maxTotal: number;
|
||||||
|
minSpawnHits: number;
|
||||||
|
maxSpawnHits: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROP_DROPS: Record<PropType, DropRule[]> = {
|
||||||
|
[PropType.Box]: [
|
||||||
|
{
|
||||||
|
resource: ResourceType.Wood,
|
||||||
|
minTotal: 1,
|
||||||
|
maxTotal: 3,
|
||||||
|
minSpawnHits: 1,
|
||||||
|
maxSpawnHits: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolvePropType(objectName: string): PropType {
|
||||||
|
const name = objectName.toLowerCase();
|
||||||
|
if (name.includes("box") || name.includes("crate") || name.includes("wood")) {
|
||||||
|
return PropType.Box;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PropType.Box;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { UpdateController } from "@24tools/playable_template";
|
||||||
|
import { Easing, Group, Tween } from "@tweenjs/tween.js";
|
||||||
|
import {
|
||||||
|
BoxGeometry,
|
||||||
|
Mesh,
|
||||||
|
MeshBasicMaterial,
|
||||||
|
Vector3,
|
||||||
|
} from "three";
|
||||||
|
import { CameraC } from "../CameraC";
|
||||||
|
import { ThreeC } from "../ThreeC";
|
||||||
|
import { ResourceInventoryC } from "./ResourceInventoryC";
|
||||||
|
import { ResourceType } from "./ResourceType";
|
||||||
|
import { ResourceUIC } from "./ResourceUIC";
|
||||||
|
|
||||||
|
const POP_DURATION_MS = 220;
|
||||||
|
const FLY_DURATION_MS = 520;
|
||||||
|
const PICKUP_SIZE = 0.14;
|
||||||
|
const STAGGER_MS = 70;
|
||||||
|
|
||||||
|
const PLACEHOLDER_COLORS: Record<ResourceType, string> = {
|
||||||
|
[ResourceType.Wood]: "#6b4423",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ResourceFlyC {
|
||||||
|
private static inited = false;
|
||||||
|
private static tweenGroup = new Group();
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
if (this.inited) return;
|
||||||
|
this.inited = true;
|
||||||
|
|
||||||
|
UpdateController.Instance.onUpdate.addDelegate(() => {
|
||||||
|
this.tweenGroup.update(performance.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static launch(origin: Vector3, type: ResourceType, index: number) {
|
||||||
|
const spawnPos = origin.clone().add(
|
||||||
|
new Vector3(
|
||||||
|
(Math.random() - 0.5) * 0.35,
|
||||||
|
0.25 + Math.random() * 0.2,
|
||||||
|
(Math.random() - 0.5) * 0.35,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.startPickup(spawnPos, type);
|
||||||
|
}, index * STAGGER_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static startPickup(worldPos: Vector3, type: ResourceType) {
|
||||||
|
const mesh = this.createWorldMesh(type);
|
||||||
|
mesh.position.copy(worldPos);
|
||||||
|
ThreeC.addToScene(mesh);
|
||||||
|
|
||||||
|
const popTarget = worldPos.clone().add(new Vector3(0, 0.35, 0));
|
||||||
|
|
||||||
|
const popTween = new Tween(mesh.position)
|
||||||
|
.to({ x: popTarget.x, y: popTarget.y, z: popTarget.z }, POP_DURATION_MS)
|
||||||
|
.easing(Easing.Quadratic.Out)
|
||||||
|
.onComplete(() => {
|
||||||
|
ThreeC.removeFromScene(mesh);
|
||||||
|
mesh.geometry.dispose();
|
||||||
|
(mesh.material as MeshBasicMaterial).dispose();
|
||||||
|
this.flyToUI(popTarget, type);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tweenGroup.add(popTween);
|
||||||
|
popTween.start(performance.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.onUpdate(() => {
|
||||||
|
element.style.left = `${state.x}px`;
|
||||||
|
element.style.top = `${state.y}px`;
|
||||||
|
element.style.transform = `translate(-50%, -50%) scale(${state.scale})`;
|
||||||
|
})
|
||||||
|
.onComplete(() => {
|
||||||
|
element.remove();
|
||||||
|
ResourceInventoryC.add(type, 1);
|
||||||
|
ResourceUIC.refresh(type);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tweenGroup.add(flyTween);
|
||||||
|
flyTween.start(performance.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createWorldMesh(type: ResourceType) {
|
||||||
|
const color = PLACEHOLDER_COLORS[type];
|
||||||
|
return new Mesh(
|
||||||
|
new BoxGeometry(PICKUP_SIZE, PICKUP_SIZE, PICKUP_SIZE),
|
||||||
|
new MeshBasicMaterial({ color }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static worldToScreen(worldPos: Vector3) {
|
||||||
|
const camera = CameraC.camera;
|
||||||
|
const projected = worldPos.clone().project(camera);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: (projected.x * 0.5 + 0.5) * window.innerWidth,
|
||||||
|
y: (-projected.y * 0.5 + 0.5) * window.innerHeight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ResourceType } from "./ResourceType";
|
||||||
|
|
||||||
|
export class ResourceInventoryC {
|
||||||
|
private static amounts = new Map<ResourceType, number>();
|
||||||
|
|
||||||
|
static get(type: ResourceType) {
|
||||||
|
return this.amounts.get(type) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static add(type: ResourceType, amount: number) {
|
||||||
|
if (amount <= 0) return;
|
||||||
|
this.amounts.set(type, this.get(type) + amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Vector3 } from "three";
|
||||||
|
import { PropC } from "../Map/PropC";
|
||||||
|
import { ResourceFlyC } from "./ResourceFlyC";
|
||||||
|
import { ResourceType } from "./ResourceType";
|
||||||
|
|
||||||
|
export class ResourceSpawnC {
|
||||||
|
static spawnFromProp(prop: PropC, type: ResourceType, count: number) {
|
||||||
|
if (count <= 0) return;
|
||||||
|
|
||||||
|
const origin = prop.object.getWorldPosition(new Vector3());
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
ResourceFlyC.launch(origin, type, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export enum ResourceType {
|
||||||
|
Wood = "wood",
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { ResourceInventoryC } from "./ResourceInventoryC";
|
||||||
|
import { ResourceType } from "./ResourceType";
|
||||||
|
|
||||||
|
type ResourceUIEntry = {
|
||||||
|
type: ResourceType;
|
||||||
|
icon: HTMLElement;
|
||||||
|
count: HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLACEHOLDER_COLORS: Record<ResourceType, string> = {
|
||||||
|
[ResourceType.Wood]: "#6b4423",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ResourceUIC {
|
||||||
|
private static entries = new Map<ResourceType, ResourceUIEntry>();
|
||||||
|
private static root: HTMLElement | null = null;
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
const uiRoot = document.getElementById("ui");
|
||||||
|
if (!uiRoot) return;
|
||||||
|
|
||||||
|
this.root = document.createElement("div");
|
||||||
|
this.root.id = "resource-bar";
|
||||||
|
this.root.className = "resource-bar";
|
||||||
|
uiRoot.appendChild(this.root);
|
||||||
|
|
||||||
|
this.register(ResourceType.Wood);
|
||||||
|
}
|
||||||
|
|
||||||
|
static register(type: ResourceType) {
|
||||||
|
if (!this.root || this.entries.has(type)) return;
|
||||||
|
|
||||||
|
const counter = document.createElement("div");
|
||||||
|
counter.className = "resource-counter";
|
||||||
|
counter.dataset.resource = type;
|
||||||
|
|
||||||
|
const icon = document.createElement("div");
|
||||||
|
icon.className = "resource-icon";
|
||||||
|
icon.style.backgroundColor = PLACEHOLDER_COLORS[type];
|
||||||
|
|
||||||
|
const count = document.createElement("span");
|
||||||
|
count.className = "resource-count";
|
||||||
|
count.textContent = "0";
|
||||||
|
|
||||||
|
counter.appendChild(icon);
|
||||||
|
counter.appendChild(count);
|
||||||
|
this.root.appendChild(counter);
|
||||||
|
|
||||||
|
this.entries.set(type, { type, icon, count });
|
||||||
|
this.refresh(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getIconCenter(type: ResourceType) {
|
||||||
|
const entry = this.entries.get(type);
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
|
const rect = entry.icon.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: rect.left + rect.width / 2,
|
||||||
|
y: rect.top + rect.height / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static refresh(type: ResourceType) {
|
||||||
|
const entry = this.entries.get(type);
|
||||||
|
if (!entry) return;
|
||||||
|
|
||||||
|
entry.count.textContent = String(ResourceInventoryC.get(type));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +1,22 @@
|
|||||||
import { BoxGeometry, Mesh, MeshStandardMaterial, Vector3 } from "three";
|
import { Vector3 } from "three";
|
||||||
import { ThreeC } from "./ThreeC";
|
|
||||||
import { InputC, JoystickC } from "@24tools/playable_template";
|
|
||||||
import { Player } from "./Presets/Player";
|
import { Player } from "./Presets/Player";
|
||||||
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";
|
|
||||||
import { Map } from "./Map/Map";
|
import { Map } from "./Map/Map";
|
||||||
|
import { GatherC } from "./Map/GatherC";
|
||||||
|
import { ResourceUIC } from "./Resources/ResourceUIC";
|
||||||
|
import { ResourceFlyC } from "./Resources/ResourceFlyC";
|
||||||
|
|
||||||
export class TestSceneC {
|
export class TestSceneC {
|
||||||
static init() {
|
static init() {
|
||||||
// this.createPrimitive();
|
ResourceUIC.init();
|
||||||
|
ResourceFlyC.init();
|
||||||
Map.Init();
|
Map.Init();
|
||||||
this.InitPlayer();
|
this.InitPlayer();
|
||||||
|
GatherC.init();
|
||||||
// example of using InputC events
|
|
||||||
// InputC.onTouchDown.addDelegate((event) => {
|
|
||||||
// console.log("onMouseDown", event);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if you have update in your controller
|
|
||||||
// UpdateController.Instance.onUpdate.addDelegate(() => {
|
|
||||||
// this.update();
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// private static createPrimitive() {
|
|
||||||
// const planeSize = new Vector3(10, 0.1, 10);
|
|
||||||
// const geometryPlane = new BoxGeometry(planeSize.x, planeSize.y, planeSize.z);
|
|
||||||
// const materialPlane = new MeshStandardMaterial({ color: 0xaaaaaa });
|
|
||||||
// const plane = new Mesh(geometryPlane, materialPlane);
|
|
||||||
|
|
||||||
// ThreeC.setShadowsStateForChildren(plane, false, true);
|
|
||||||
|
|
||||||
// plane.position.y = -planeSize.y / 2;
|
|
||||||
|
|
||||||
// new PhysicsBody(
|
|
||||||
// plane,
|
|
||||||
// false,
|
|
||||||
// 0,
|
|
||||||
// PhysicsLayer.Wall,
|
|
||||||
// PhysicsLayer.Player,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// ThreeC.addToScene(plane);
|
|
||||||
// }
|
|
||||||
|
|
||||||
private static InitPlayer() {
|
private static InitPlayer() {
|
||||||
const playerSpawnPoint = new Vector3(4, 0, 27);
|
const playerSpawnPoint = new Vector3(4, 0, 27);
|
||||||
Player.SetSpawnPosition(playerSpawnPoint);
|
Player.SetSpawnPosition(playerSpawnPoint);
|
||||||
Player.Init();
|
Player.Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-3
@@ -1,4 +1,46 @@
|
|||||||
#ui {
|
.resource-bar {
|
||||||
/* flex-basis: 60%; */
|
position: fixed;
|
||||||
flex-grow: 1;
|
top: 2vh;
|
||||||
|
left: 12vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-counter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1vw;
|
||||||
|
padding: 0.6vh 1.2vw;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
border-radius: 0.8vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-icon {
|
||||||
|
width: 4vh;
|
||||||
|
height: 4vh;
|
||||||
|
border-radius: 0.5vh;
|
||||||
|
border: 0.2vh solid rgba(255, 255, 255, 0.35);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-count {
|
||||||
|
min-width: 2ch;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 3vh;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-fly-pickup {
|
||||||
|
position: fixed;
|
||||||
|
width: 3vh;
|
||||||
|
height: 3vh;
|
||||||
|
border-radius: 0.4vh;
|
||||||
|
border: 0.15vh solid rgba(255, 255, 255, 0.5);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ window.setupConfig = async function (config) {
|
|||||||
redirectOptions: {},
|
redirectOptions: {},
|
||||||
ticker: Template3d.ticker,
|
ticker: Template3d.ticker,
|
||||||
debug: {
|
debug: {
|
||||||
physics: true,
|
physics: false,
|
||||||
// set true if you want to enable physics debugger
|
// set true if you want to enable physics debugger
|
||||||
logger: false // set true if you want to enable logger
|
logger: false // set true if you want to enable logger
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user