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 { 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 {
|
||||
static Init() {
|
||||
@@ -14,19 +20,109 @@ export class Map {
|
||||
ThreeC.setShadowsStateForChildren(mapObject, true, true);
|
||||
ThreeC.addToScene(mapObject);
|
||||
|
||||
const colliders = mapObject.getObjectByName("Colliders");
|
||||
if (colliders) {
|
||||
colliders.visible = false;
|
||||
this.buildColliders(colliders);
|
||||
PhysicsTriggerC.init();
|
||||
this.buildPhysics(mapObject);
|
||||
}
|
||||
|
||||
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) {
|
||||
collidersRoot.traverse((child) => {
|
||||
if (child === collidersRoot) return;
|
||||
if ((child as any).isMesh) {
|
||||
private static addWallPhysics(root: Object3D) {
|
||||
root.traverse((child) => {
|
||||
if (child === root) return;
|
||||
if ((child as Mesh).isMesh) {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
let oldClipAction: null | AnimationAction = this.curClipAction;
|
||||
var clipAction = this.animMixer.clipAction(this.animationList[anim_id]);
|
||||
|
||||
@@ -2,4 +2,9 @@ export enum BaseAnimation {
|
||||
|
||||
Nan = -1,
|
||||
|
||||
Idle = 0,
|
||||
|
||||
Run = 1,
|
||||
|
||||
WeaponTakeOut = 2,
|
||||
|
||||
@@ -46,6 +46,7 @@ export class PlayerInput implements IMoveInput {
|
||||
private static threshold = 0.25;
|
||||
|
||||
get CurrentDirection() { return this.currentDirection.clone(); };
|
||||
get IsActive() { return this.inputaActive; };
|
||||
|
||||
inputaActive: boolean = false;
|
||||
|
||||
|
||||
@@ -22,12 +22,14 @@ export class RotationC {
|
||||
|
||||
private update(delta: number) {
|
||||
const loockAtStep = this.input.CurrentDirection;
|
||||
if (loockAtStep.length() != 0) {
|
||||
const loockAtPoint = this.target.position.clone().add(loockAtStep);
|
||||
this.target.lookAt(loockAtPoint);
|
||||
this.targetQ.copy(this.target.quaternion);
|
||||
this.target.quaternion.copy(this.currentQ);
|
||||
}
|
||||
if (loockAtStep.length() == 0) return;
|
||||
|
||||
this.currentQ.copy(this.target.quaternion);
|
||||
|
||||
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.currentQ.copy(this.target.quaternion);
|
||||
|
||||
@@ -5,22 +5,27 @@ import { MeshType } from "./Enums/MeshType";
|
||||
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
|
||||
import { BaseAnimation } from "./Enums/BaseAnimation";
|
||||
import { PhysicsBody, PhysicsLayer } from "../PhysicsC";
|
||||
import { Object3D } from "three";
|
||||
import { Object3D, Quaternion, Vector3 } from "three";
|
||||
import { ThreeC } from "../ThreeC";
|
||||
import { PlayerInput } from "./Input/PlayerInput";
|
||||
import { MoveC } from "./Movment/MoveC";
|
||||
import { Vec3 } from "cannon-es";
|
||||
import { contain } from "three/src/extras/TextureUtils";
|
||||
import { Vector3CToT, Vector3TToC } from "./Helper";
|
||||
import { RotationC } from "./Movment/RotationC";
|
||||
import { FollowCameraC } from "./Movment/CameraMovment/FollowCamera";
|
||||
import { Vector3 } from "three";
|
||||
|
||||
export class Player {
|
||||
private static inited: 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 onAttackComplete: (() => void) | null = null;
|
||||
private static onTurnComplete: (() => void) | null = null;
|
||||
private static turnTargetQ = new Quaternion();
|
||||
|
||||
private static container: Object3D = new Object3D;
|
||||
private static input: PlayerInput;
|
||||
@@ -28,6 +33,9 @@ export class Player {
|
||||
private static rotation: RotationC;
|
||||
private static spawnPosition: Vector3 = new Vector3(0, 0, 0);
|
||||
|
||||
private static readonly turnSpeed = 8;
|
||||
private static readonly turnCompleteAngle = 0.05;
|
||||
|
||||
static character: Character;
|
||||
static physics: PhysicsBody;
|
||||
|
||||
@@ -37,21 +45,24 @@ export class Player {
|
||||
|
||||
static Init() {
|
||||
if (this.inited) return;
|
||||
this.inited = true
|
||||
const asset = ResourcesC.getResource<GLTF>(ResourcesType.Mesh, MeshType.Character)
|
||||
this.inited = true;
|
||||
const asset = ResourcesC.getResource<GLTF>(ResourcesType.Mesh, MeshType.Character);
|
||||
this.character = new Character(asset);
|
||||
|
||||
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);
|
||||
ThreeC.addToScene(this.container);
|
||||
|
||||
this.InitPhisic();
|
||||
const input = new PlayerInput();
|
||||
this.input = new PlayerInput();
|
||||
const moveSpeed = 3;
|
||||
const rotationSpeed = 8;
|
||||
this.movement = new MoveC(input, moveSpeed);
|
||||
this.rotation = new RotationC(this.container, input, rotationSpeed);
|
||||
this.movement = new MoveC(this.input, moveSpeed);
|
||||
this.rotation = new RotationC(this.container, this.input, rotationSpeed);
|
||||
|
||||
this.character.onAnimFinish.addDelegate(() => this.OnCombatAnimationFinished());
|
||||
|
||||
this.updateDelegate = new Delegate<number>((delta) => this.Update(delta));
|
||||
UpdateController.Instance.onUpdate.addListener(this.updateDelegate);
|
||||
@@ -59,23 +70,136 @@ export class Player {
|
||||
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() {
|
||||
this.physics = new PhysicsBody(
|
||||
this.container,
|
||||
false,
|
||||
1,
|
||||
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() {
|
||||
if (this.isRunning) return;
|
||||
if (this.isRunning || this.isAttacking || this.isTurningToTarget) return;
|
||||
this.character.playAnimation(BaseAnimation.Run);
|
||||
this.isRunning = true;
|
||||
}
|
||||
|
||||
private static StopRunning() {
|
||||
if (!this.isRunning) return;
|
||||
if (!this.isRunning || this.isAttacking || this.isTurningToTarget) return;
|
||||
this.character.playAnimation(BaseAnimation.Idle);
|
||||
this.AnimationValue = 1;
|
||||
this.isRunning = false;
|
||||
@@ -87,6 +211,23 @@ export class Player {
|
||||
}
|
||||
|
||||
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 weight = this.movement.Weight;
|
||||
|
||||
@@ -98,20 +239,17 @@ export class Player {
|
||||
this.StopRunning();
|
||||
}
|
||||
|
||||
|
||||
|
||||
const cPos = Vector3TToC(diraction);
|
||||
|
||||
this.physics.getPhysicsBody().velocity.copy(cPos);
|
||||
this.physics.getPhysicsBody().wakeUp();
|
||||
this.MoveVisual(delta);
|
||||
}
|
||||
|
||||
private static MoveVisual(delta: number) {
|
||||
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 { ThreeC } from "./ThreeC";
|
||||
import { InputC, JoystickC } from "@24tools/playable_template";
|
||||
import { Vector3 } from "three";
|
||||
import { Player } from "./Presets/Player";
|
||||
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";
|
||||
import { Map } from "./Map/Map";
|
||||
import { GatherC } from "./Map/GatherC";
|
||||
import { ResourceUIC } from "./Resources/ResourceUIC";
|
||||
import { ResourceFlyC } from "./Resources/ResourceFlyC";
|
||||
|
||||
export class TestSceneC {
|
||||
static init() {
|
||||
// this.createPrimitive();
|
||||
ResourceUIC.init();
|
||||
ResourceFlyC.init();
|
||||
Map.Init();
|
||||
this.InitPlayer();
|
||||
|
||||
// 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();
|
||||
// });
|
||||
GatherC.init();
|
||||
}
|
||||
|
||||
// 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() {
|
||||
const playerSpawnPoint = new Vector3(4, 0, 27);
|
||||
Player.SetSpawnPosition(playerSpawnPoint);
|
||||
Player.Init();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+45
-3
@@ -1,4 +1,46 @@
|
||||
#ui {
|
||||
/* flex-basis: 60%; */
|
||||
flex-grow: 1;
|
||||
.resource-bar {
|
||||
position: fixed;
|
||||
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: {},
|
||||
ticker: Template3d.ticker,
|
||||
debug: {
|
||||
physics: true,
|
||||
physics: false,
|
||||
// set true if you want to enable physics debugger
|
||||
logger: false // set true if you want to enable logger
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user