fixed bugs
This commit is contained in:
@@ -67,7 +67,7 @@ export const globalSettings: ConfigUiParamsCategories[] = [
|
|||||||
[
|
[
|
||||||
-360,
|
-360,
|
||||||
360,
|
360,
|
||||||
0
|
2
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-360,
|
-360,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { EasyEvent, JoystickC, UpdateController } from "@24tools/playable_templa
|
|||||||
import { Vector3 } from "three";
|
import { Vector3 } from "three";
|
||||||
import { PhysicsTriggerC } from "./PhysicsTriggerC";
|
import { PhysicsTriggerC } from "./PhysicsTriggerC";
|
||||||
import { PropC, PropRegistry } from "./PropC";
|
import { PropC, PropRegistry } from "./PropC";
|
||||||
import { PropHpUIC } from "./PropHpUIC";
|
|
||||||
import { Player } from "../Presets/Player";
|
import { Player } from "../Presets/Player";
|
||||||
|
|
||||||
export class GatherC {
|
export class GatherC {
|
||||||
@@ -120,7 +119,6 @@ export class GatherC {
|
|||||||
if (Player.isMoving() || Player.isCombatBusy()) return;
|
if (Player.isMoving() || Player.isCombatBusy()) return;
|
||||||
|
|
||||||
const targetPosition = this.getTargetPosition(props);
|
const targetPosition = this.getTargetPosition(props);
|
||||||
PropHpUIC.showFor(props);
|
|
||||||
Player.startAutoAttack(
|
Player.startAutoAttack(
|
||||||
() => this.onAttackStrike(),
|
() => this.onAttackStrike(),
|
||||||
() => {},
|
() => {},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
import { Box3, Object3D, Vector3 } from "three";
|
import { Box3, Object3D, Vector3 } from "three";
|
||||||
import { Body } from "cannon-es";
|
import { Body } from "cannon-es";
|
||||||
|
|
||||||
const PLAYER_RADIUS = 0.3;
|
const PLAYER_RADIUS = 0.5;
|
||||||
|
|
||||||
export type TriggerEventPayload = {
|
export type TriggerEventPayload = {
|
||||||
lootableObject: Object3D;
|
lootableObject: Object3D;
|
||||||
@@ -53,7 +53,7 @@ export class PhysicsTriggerC {
|
|||||||
lootableObject,
|
lootableObject,
|
||||||
triggerObject,
|
triggerObject,
|
||||||
center,
|
center,
|
||||||
radius: Math.max(size.x, size.y, size.z) / 2,
|
radius: Math.max(size.x, size.z) / 2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +90,8 @@ export class PhysicsTriggerC {
|
|||||||
|
|
||||||
for (const trigger of this.triggers) {
|
for (const trigger of this.triggers) {
|
||||||
const dx = playerBody.position.x - trigger.center.x;
|
const dx = playerBody.position.x - trigger.center.x;
|
||||||
const dy = playerBody.position.y - trigger.center.y;
|
|
||||||
const dz = playerBody.position.z - trigger.center.z;
|
const dz = playerBody.position.z - trigger.center.z;
|
||||||
const distanceSq = dx * dx + dy * dy + dz * dz;
|
const distanceSq = dx * dx + 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);
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ type FillPart = {
|
|||||||
node: Object3D;
|
node: Object3D;
|
||||||
baseScaleX: number;
|
baseScaleX: number;
|
||||||
basePosX: number;
|
basePosX: number;
|
||||||
/** Local mesh min.x (negative); keeps left edge fixed when scaling. */
|
/** Local mesh max.x; keeps right edge fixed when scaling. */
|
||||||
anchorMinX: number;
|
anchorMaxX: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PropHpBar {
|
export class PropHpBar {
|
||||||
@@ -59,6 +59,7 @@ export class PropHpBar {
|
|||||||
|
|
||||||
private displayedHealth: number;
|
private displayedHealth: number;
|
||||||
private visible = false;
|
private visible = false;
|
||||||
|
private hasTakenDamage = false;
|
||||||
|
|
||||||
static getPrefab(): Object3D | null {
|
static getPrefab(): Object3D | null {
|
||||||
const fromScene = ThreeC.getObject(HP_BAR_MESH);
|
const fromScene = ThreeC.getObject(HP_BAR_MESH);
|
||||||
@@ -108,6 +109,8 @@ export class PropHpBar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDamage() {
|
onDamage() {
|
||||||
|
this.hasTakenDamage = true;
|
||||||
|
|
||||||
if (!this.visible) {
|
if (!this.visible) {
|
||||||
this.setVisible(true);
|
this.setVisible(true);
|
||||||
return;
|
return;
|
||||||
@@ -137,6 +140,9 @@ export class PropHpBar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setVisible(value: boolean) {
|
setVisible(value: boolean) {
|
||||||
|
// Once the prop has been hit, keep its bar visible until it breaks.
|
||||||
|
if (!value && this.hasTakenDamage) return;
|
||||||
|
|
||||||
this.visible = value;
|
this.visible = value;
|
||||||
this.root.visible = value;
|
this.root.visible = value;
|
||||||
|
|
||||||
@@ -164,11 +170,10 @@ export class PropHpBar {
|
|||||||
const clamped = Math.max(0, Math.min(1, ratio));
|
const clamped = Math.max(0, Math.min(1, ratio));
|
||||||
part.node.scale.x = part.baseScaleX * clamped;
|
part.node.scale.x = part.baseScaleX * clamped;
|
||||||
part.node.position.x =
|
part.node.position.x =
|
||||||
part.basePosX + part.anchorMinX * part.baseScaleX * (1 - clamped);
|
part.basePosX + part.anchorMaxX * part.baseScaleX * (1 - clamped);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createFillPart(node: Object3D): FillPart {
|
private createFillPart(node: Object3D): FillPart {
|
||||||
const meshMinX = this.getMeshMinX(node);
|
|
||||||
const isForeground = node.name.includes(NODE_FOREGROUND);
|
const isForeground = node.name.includes(NODE_FOREGROUND);
|
||||||
|
|
||||||
node.renderOrder = isForeground
|
node.renderOrder = isForeground
|
||||||
@@ -179,16 +184,16 @@ export class PropHpBar {
|
|||||||
node,
|
node,
|
||||||
baseScaleX: node.scale.x,
|
baseScaleX: node.scale.x,
|
||||||
basePosX: node.position.x,
|
basePosX: node.position.x,
|
||||||
anchorMinX: meshMinX,
|
anchorMaxX: this.getMeshMaxX(node),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMeshMinX(node: Object3D) {
|
private getMeshMaxX(node: Object3D) {
|
||||||
const mesh = node as Mesh;
|
const mesh = node as Mesh;
|
||||||
if (!mesh.isMesh || !mesh.geometry) return -0.935;
|
if (!mesh.isMesh || !mesh.geometry) return 0;
|
||||||
|
|
||||||
mesh.geometry.computeBoundingBox();
|
mesh.geometry.computeBoundingBox();
|
||||||
return mesh.geometry.boundingBox?.min.x ?? -0.935;
|
return mesh.geometry.boundingBox?.max.x ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getHealthRatio(health: number) {
|
private getHealthRatio(health: number) {
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { EasyEvent } from "@24tools/playable_template";
|
import { EasyEvent, UpdateController } from "@24tools/playable_template";
|
||||||
import { AnimationAction, AnimationClip, AnimationMixer, LoopOnce, LoopRepeat, 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";
|
||||||
|
import { Easing, Group, Tween } from "@tweenjs/tween.js";
|
||||||
|
|
||||||
|
/** Duration of the bat pop-in / pop-out scale animation. */
|
||||||
|
const WEAPON_SWAP_MS = 140;
|
||||||
|
/** Near-zero scale used instead of exact 0 to keep normals/shadows valid. */
|
||||||
|
const HIDDEN_SCALE = 0.001;
|
||||||
|
|
||||||
export class Character {
|
export class Character {
|
||||||
tObj: Object3D;
|
tObj: Object3D;
|
||||||
@@ -14,6 +20,14 @@ export class Character {
|
|||||||
onAnimLoop: EasyEvent<{}> = new EasyEvent<{}>();
|
onAnimLoop: EasyEvent<{}> = new EasyEvent<{}>();
|
||||||
onAnimFinish: EasyEvent<{}> = new EasyEvent<{}>();
|
onAnimFinish: EasyEvent<{}> = new EasyEvent<{}>();
|
||||||
|
|
||||||
|
private weaponTweens = new Group();
|
||||||
|
private tool1?: Object3D;
|
||||||
|
private tool2?: Object3D;
|
||||||
|
private tool1FullScale = new Vector3(1, 1, 1);
|
||||||
|
private tool2FullScale = new Vector3(1, 1, 1);
|
||||||
|
private tool1Tween: Tween<Vector3> | null = null;
|
||||||
|
private tool2Tween: Tween<Vector3> | null = null;
|
||||||
|
|
||||||
constructor(prefab: GLTF, start_position = new Vector3()) {
|
constructor(prefab: GLTF, start_position = new Vector3()) {
|
||||||
const tObj = clone(prefab.scene);
|
const tObj = clone(prefab.scene);
|
||||||
|
|
||||||
@@ -35,18 +49,35 @@ export class Character {
|
|||||||
|
|
||||||
ThreeC.addAnimMixer(animMixer);
|
ThreeC.addAnimMixer(animMixer);
|
||||||
|
|
||||||
|
this.cacheWeapons();
|
||||||
|
UpdateController.Instance.onUpdate.addDelegate(() => {
|
||||||
|
this.weaponTweens.update(performance.now());
|
||||||
|
});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
set AnimationSpeed(timeScale: number) {
|
private cacheWeapons() {
|
||||||
if (this.curClipAction) {
|
this.tObj.traverse((child) => {
|
||||||
this.curClipAction.timeScale = timeScale;
|
if (child.name === "Tool_1") {
|
||||||
}
|
this.tool1 = child;
|
||||||
}
|
this.tool1FullScale.copy(child.scale);
|
||||||
|
}
|
||||||
|
if (child.name === "Tool_2") {
|
||||||
|
this.tool2 = child;
|
||||||
|
this.tool2FullScale.copy(child.scale);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
set AnimationWeight(weight: number) {
|
// Resting state: bat sits on the back (Tool_2), hand tool (Tool_1) hidden.
|
||||||
if (this.curClipAction) {
|
// Snapping here avoids a startup pop when setDefaultWeapons runs on init.
|
||||||
this.curClipAction.weight = weight;
|
if (this.tool1) {
|
||||||
|
this.tool1.scale.set(HIDDEN_SCALE, HIDDEN_SCALE, HIDDEN_SCALE);
|
||||||
|
this.tool1.visible = false;
|
||||||
|
}
|
||||||
|
if (this.tool2) {
|
||||||
|
this.tool2.scale.copy(this.tool2FullScale);
|
||||||
|
this.tool2.visible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,15 +90,72 @@ export class Character {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDefaultWeapons() {
|
setDefaultWeapons() {
|
||||||
this.setObjectVisible("Tool_1", false);
|
|
||||||
this.setObjectVisible("Tool_2", true);
|
|
||||||
this.setObjectVisible("Character_Pistol", false);
|
this.setObjectVisible("Character_Pistol", false);
|
||||||
|
// Bat goes back to the spine: shrink the hand tool, grow the back tool.
|
||||||
|
this.swapTool(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setBatEquipped(equipped: boolean) {
|
setBatEquipped(equipped: boolean) {
|
||||||
this.setObjectVisible("Tool_1", equipped);
|
|
||||||
this.setObjectVisible("Tool_2", !equipped);
|
|
||||||
this.setObjectVisible("Character_Pistol", false);
|
this.setObjectVisible("Character_Pistol", false);
|
||||||
|
this.swapTool(equipped);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** equipped=true -> bat appears in hands (Tool_1) and vanishes from back (Tool_2). */
|
||||||
|
private swapTool(equipped: boolean) {
|
||||||
|
this.tool1Tween = this.animateToolScale(
|
||||||
|
this.tool1,
|
||||||
|
this.tool1FullScale,
|
||||||
|
equipped,
|
||||||
|
this.tool1Tween,
|
||||||
|
);
|
||||||
|
this.tool2Tween = this.animateToolScale(
|
||||||
|
this.tool2,
|
||||||
|
this.tool2FullScale,
|
||||||
|
!equipped,
|
||||||
|
this.tool2Tween,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private animateToolScale(
|
||||||
|
tool: Object3D | undefined,
|
||||||
|
fullScale: Vector3,
|
||||||
|
appear: boolean,
|
||||||
|
activeTween: Tween<Vector3> | null,
|
||||||
|
): Tween<Vector3> | null {
|
||||||
|
if (!tool) return null;
|
||||||
|
|
||||||
|
// Interruption: kill any in-flight tween for this node before retargeting,
|
||||||
|
// otherwise the scale can get stuck mid-animation on rapid combat toggles.
|
||||||
|
if (activeTween) {
|
||||||
|
this.weaponTweens.remove(activeTween);
|
||||||
|
activeTween.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = appear
|
||||||
|
? { x: fullScale.x, y: fullScale.y, z: fullScale.z }
|
||||||
|
: { x: HIDDEN_SCALE, y: HIDDEN_SCALE, z: HIDDEN_SCALE };
|
||||||
|
|
||||||
|
if (appear) {
|
||||||
|
tool.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tween = new Tween(tool.scale, this.weaponTweens)
|
||||||
|
.to(target, WEAPON_SWAP_MS)
|
||||||
|
.easing(appear ? Easing.Quadratic.Out : Easing.Quadratic.In)
|
||||||
|
.onComplete(() => {
|
||||||
|
if (!appear) {
|
||||||
|
tool.visible = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.start(performance.now());
|
||||||
|
|
||||||
|
return tween;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentTimeScale(scale: number) {
|
||||||
|
if (this.curClipAction) {
|
||||||
|
this.curClipAction.timeScale = scale;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isPlayingAnimation(anim_id: number) {
|
isPlayingAnimation(anim_id: number) {
|
||||||
|
|||||||
@@ -9,18 +9,26 @@ 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 } 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 { PhysicsTriggerC } from "../Map/PhysicsTriggerC";
|
||||||
import { PropHpUIC } from "../Map/PropHpUIC";
|
import { PropHpUIC } from "../Map/PropHpUIC";
|
||||||
|
import { PlayerContext } from "./PlayerState/PlayerContext";
|
||||||
|
import { PlayerStateMachine } from "./PlayerState/PlayerStateMachine";
|
||||||
|
import { PlayerStateType } from "./PlayerState/PlayerStateType";
|
||||||
|
import { IdleState } from "./PlayerState/states/IdleState";
|
||||||
|
import { RunState } from "./PlayerState/states/RunState";
|
||||||
|
import { TurnToTargetState } from "./PlayerState/states/TurnToTargetState";
|
||||||
|
import { LootState } from "./PlayerState/states/LootState";
|
||||||
|
import { COMBAT_EXIT_FADE } from "./PlayerState/combatConstants";
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
private static inited = false;
|
private static inited = false;
|
||||||
private static isRunning = false;
|
|
||||||
|
|
||||||
private static updateDelegate: Delegate<number>;
|
private static updateDelegate: Delegate<number>;
|
||||||
|
private static context: PlayerContext;
|
||||||
|
private static stateMachine: PlayerStateMachine;
|
||||||
|
|
||||||
private static container: Object3D = new Object3D();
|
private static container: Object3D = new Object3D();
|
||||||
private static input: PlayerInput;
|
private static input: PlayerInput;
|
||||||
@@ -40,7 +48,6 @@ export class Player {
|
|||||||
|
|
||||||
this.container.position.copy(this.spawnPosition);
|
this.container.position.copy(this.spawnPosition);
|
||||||
this.character.setDefaultWeapons();
|
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);
|
||||||
|
|
||||||
@@ -53,24 +60,35 @@ export class Player {
|
|||||||
this.movement = new MoveC(this.input, moveSpeed);
|
this.movement = new MoveC(this.input, moveSpeed);
|
||||||
this.rotation = new RotationC(this.container, rotationSpeed);
|
this.rotation = new RotationC(this.container, rotationSpeed);
|
||||||
|
|
||||||
PlayerCombat.init({
|
this.context = new PlayerContext({
|
||||||
character: this.character,
|
character: this.character,
|
||||||
getPhysicsBody: () => this.physics.getPhysicsBody(),
|
movement: this.movement,
|
||||||
isMoving: () => this.isMoving(),
|
rotation: this.rotation,
|
||||||
onStopRunning: () => this.stopRunning(),
|
physics: this.physics,
|
||||||
onResumeRunning: () => this.resumeRunning(),
|
container: this.container,
|
||||||
onSetTurnTarget: (worldPosition) => this.rotation.setTargetWorldPosition(worldPosition),
|
|
||||||
onUpdateRotation: (delta) => this.rotation.update(delta),
|
|
||||||
isRotationComplete: () => this.rotation.isComplete(),
|
|
||||||
onSyncRotation: () => this.syncRotationToFacing(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.stateMachine = new PlayerStateMachine(
|
||||||
|
this.context,
|
||||||
|
new Map<PlayerStateType, IdleState | RunState | TurnToTargetState | LootState>([
|
||||||
|
[PlayerStateType.Idle, new IdleState()],
|
||||||
|
[PlayerStateType.Run, new RunState()],
|
||||||
|
[PlayerStateType.TurnToTarget, new TurnToTargetState()],
|
||||||
|
[PlayerStateType.Loot, new LootState()],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
this.context.stateMachine = this.stateMachine;
|
||||||
|
|
||||||
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 get currentState(): PlayerStateType {
|
||||||
|
return this.stateMachine.currentState;
|
||||||
|
}
|
||||||
|
|
||||||
static getWorldPosition() {
|
static getWorldPosition() {
|
||||||
return this.container.getWorldPosition(new Vector3());
|
return this.container.getWorldPosition(new Vector3());
|
||||||
}
|
}
|
||||||
@@ -79,113 +97,103 @@ export class Player {
|
|||||||
return this.movement.Direction.lengthSq() > 0;
|
return this.movement.Direction.lengthSq() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isInCombat() {
|
||||||
|
const state = this.currentState;
|
||||||
|
return state === PlayerStateType.TurnToTarget || state === PlayerStateType.Loot;
|
||||||
|
}
|
||||||
|
|
||||||
static isCombatBusy() {
|
static isCombatBusy() {
|
||||||
return PlayerCombat.isBusy;
|
return this.isInCombat() || this.context.pendingCombatExit;
|
||||||
}
|
}
|
||||||
|
|
||||||
static isAutoAttackActive() {
|
static isAutoAttackActive() {
|
||||||
return PlayerCombat.isAutoAttackActive;
|
return this.context.isAutoAttacking;
|
||||||
}
|
}
|
||||||
|
|
||||||
static startAutoAttack(
|
static startAutoAttack(
|
||||||
onStrike: () => void,
|
onStrike: () => void,
|
||||||
onComplete: () => void,
|
_onComplete: () => void,
|
||||||
targetWorldPosition: Vector3,
|
targetWorldPosition: Vector3,
|
||||||
) {
|
) {
|
||||||
PlayerCombat.startAutoAttack(onStrike, onComplete, targetWorldPosition);
|
if (this.context.isAutoAttacking) return;
|
||||||
|
|
||||||
|
this.context.isAutoAttacking = true;
|
||||||
|
this.context.onStrike = onStrike;
|
||||||
|
this.context.combatTargetWorldPosition.copy(targetWorldPosition);
|
||||||
|
this.rotation.setTargetWorldPosition(targetWorldPosition);
|
||||||
|
this.character.setBatEquipped(true);
|
||||||
|
this.stateMachine.setState(PlayerStateType.Loot);
|
||||||
}
|
}
|
||||||
|
|
||||||
static stopAutoAttack() {
|
static stopAutoAttack() {
|
||||||
PlayerCombat.stopAutoAttack();
|
const needsExit = this.context.isAutoAttacking
|
||||||
|
|| this.currentState === PlayerStateType.TurnToTarget
|
||||||
|
|| this.currentState === PlayerStateType.Loot
|
||||||
|
|| this.context.pendingCombatExit;
|
||||||
|
|
||||||
|
this.context.isAutoAttacking = false;
|
||||||
|
this.context.onStrike = null;
|
||||||
|
this.context.pendingCombatExit = false;
|
||||||
|
|
||||||
|
if (needsExit) {
|
||||||
|
this.finishCombatExit();
|
||||||
|
}
|
||||||
|
|
||||||
PropHpUIC.hideAll();
|
PropHpUIC.hideAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
static retargetAutoAttack(targetWorldPosition: Vector3) {
|
static retargetAutoAttack(targetWorldPosition: Vector3) {
|
||||||
PlayerCombat.retargetAutoAttack(targetWorldPosition);
|
if (!this.context.isAutoAttacking) return;
|
||||||
|
|
||||||
|
this.context.combatTargetWorldPosition.copy(targetWorldPosition);
|
||||||
|
this.context.zeroVelocity();
|
||||||
|
this.rotation.setTargetWorldPosition(targetWorldPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
static playAttack(onStrike: () => void) {
|
static playAttack(onStrike: () => void) {
|
||||||
PlayerCombat.playAttack(onStrike);
|
this.context.onStrike = onStrike;
|
||||||
|
this.stateMachine.setState(PlayerStateType.Loot);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static initPhysics() {
|
private static initPhysics() {
|
||||||
|
const playerColliderRadius = 0.2;
|
||||||
this.physics = new PhysicsBody(
|
this.physics = new PhysicsBody(
|
||||||
this.container,
|
this.container,
|
||||||
false,
|
false,
|
||||||
1,
|
1,
|
||||||
PhysicsLayer.Player,
|
PhysicsLayer.Player,
|
||||||
PhysicsLayer.Wall | PhysicsLayer.Trigger,
|
PhysicsLayer.Wall | PhysicsLayer.Trigger,
|
||||||
|
playerColliderRadius,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static startRunning() {
|
private static finishCombatExit() {
|
||||||
if (PlayerCombat.isBusy) return;
|
this.context.pendingCombatExit = false;
|
||||||
if (this.isRunning) return;
|
this.context.character.setDefaultWeapons();
|
||||||
if (this.character.isPlayingAnimation(BaseAnimation.Run)) {
|
this.context.syncRotation();
|
||||||
this.isRunning = true;
|
|
||||||
|
if (this.isMoving()) {
|
||||||
|
this.stateMachine.setState(PlayerStateType.Run);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.character.playAnimation(BaseAnimation.Run);
|
|
||||||
this.isRunning = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static resumeRunning() {
|
this.context.character.crossFadeToAnimation(BaseAnimation.Idle, false, COMBAT_EXIT_FADE);
|
||||||
if (PlayerCombat.isBusy) return;
|
this.context.skipStateEnterAnimation = true;
|
||||||
this.isRunning = true;
|
this.stateMachine.setState(PlayerStateType.Idle);
|
||||||
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.animationValue = 1;
|
|
||||||
this.isRunning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static set animationValue(value: number) {
|
|
||||||
this.character.AnimationSpeed = value;
|
|
||||||
this.character.AnimationWeight = value * 12.5 + 87.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static update(delta: number) {
|
private static update(delta: number) {
|
||||||
this.movement.update(delta);
|
this.movement.update(delta);
|
||||||
|
|
||||||
if (this.isMoving()) {
|
if (this.isMoving() && (this.context.isAutoAttacking || this.currentState === PlayerStateType.TurnToTarget)) {
|
||||||
PlayerCombat.cancelOnMoveInput();
|
this.stopAutoAttack();
|
||||||
}
|
|
||||||
|
|
||||||
if (PlayerCombat.update(delta)) {
|
|
||||||
this.syncVisual(delta);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const direction = this.movement.Direction;
|
|
||||||
const weight = this.movement.Weight;
|
|
||||||
const isMoving = direction.lengthSq() > 0;
|
|
||||||
|
|
||||||
if (isMoving) {
|
|
||||||
this.startRunning();
|
|
||||||
this.animationValue = weight;
|
|
||||||
FollowCameraC.inputDirection.copy(direction).normalize();
|
|
||||||
this.rotation.setTargetDirection(direction);
|
|
||||||
} else {
|
|
||||||
this.stopRunning();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.stateMachine.update(delta);
|
||||||
this.rotation.update(delta);
|
this.rotation.update(delta);
|
||||||
|
|
||||||
this.physics.getPhysicsBody().velocity.copy(Vector3TToC(direction));
|
|
||||||
this.physics.getPhysicsBody().wakeUp();
|
|
||||||
this.syncVisual(delta);
|
this.syncVisual(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static syncRotationToFacing() {
|
|
||||||
this.rotation.syncToCurrentFacing();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static syncVisual(delta: number) {
|
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);
|
||||||
|
|||||||
@@ -1,273 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { PlayerContext } from "./PlayerContext";
|
||||||
|
|
||||||
|
export interface IPlayerState {
|
||||||
|
enter(context: PlayerContext): void;
|
||||||
|
exit(context: PlayerContext): void;
|
||||||
|
update(context: PlayerContext, deltaTime: number): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Object3D, Vector3 } from "three";
|
||||||
|
import { Character } from "../Character/Character";
|
||||||
|
import { MoveC } from "../Movment/MoveC";
|
||||||
|
import { RotationC } from "../Movment/RotationC";
|
||||||
|
import { PhysicsBody } from "../../PhysicsC";
|
||||||
|
import { PlayerStateMachine } from "./PlayerStateMachine";
|
||||||
|
|
||||||
|
export class PlayerContext {
|
||||||
|
stateMachine!: PlayerStateMachine;
|
||||||
|
|
||||||
|
readonly character: Character;
|
||||||
|
readonly movement: MoveC;
|
||||||
|
readonly rotation: RotationC;
|
||||||
|
readonly physics: PhysicsBody;
|
||||||
|
readonly container: Object3D;
|
||||||
|
|
||||||
|
isAutoAttacking = false;
|
||||||
|
pendingCombatExit = false;
|
||||||
|
skipStateEnterAnimation = false;
|
||||||
|
|
||||||
|
onStrike: (() => void) | null = null;
|
||||||
|
readonly combatTargetWorldPosition = new Vector3();
|
||||||
|
|
||||||
|
constructor(opts: {
|
||||||
|
character: Character;
|
||||||
|
movement: MoveC;
|
||||||
|
rotation: RotationC;
|
||||||
|
physics: PhysicsBody;
|
||||||
|
container: Object3D;
|
||||||
|
}) {
|
||||||
|
this.character = opts.character;
|
||||||
|
this.movement = opts.movement;
|
||||||
|
this.rotation = opts.rotation;
|
||||||
|
this.physics = opts.physics;
|
||||||
|
this.container = opts.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMoving() {
|
||||||
|
return this.movement.Direction.lengthSq() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
zeroVelocity() {
|
||||||
|
this.physics.getPhysicsBody().velocity.set(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncRotation() {
|
||||||
|
this.rotation.syncToCurrentFacing();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { PlayerContext } from "./PlayerContext";
|
||||||
|
import { IPlayerState } from "./IPlayerState";
|
||||||
|
import { PlayerStateType } from "./PlayerStateType";
|
||||||
|
|
||||||
|
export class PlayerStateMachine {
|
||||||
|
private activeState: IPlayerState;
|
||||||
|
private currentStateType: PlayerStateType;
|
||||||
|
private readonly states: Map<PlayerStateType, IPlayerState>;
|
||||||
|
|
||||||
|
get currentState(): PlayerStateType {
|
||||||
|
return this.currentStateType;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly context: PlayerContext,
|
||||||
|
states: Map<PlayerStateType, IPlayerState>,
|
||||||
|
initialState: PlayerStateType = PlayerStateType.Idle,
|
||||||
|
) {
|
||||||
|
this.states = states;
|
||||||
|
this.activeState = states.get(initialState)!;
|
||||||
|
this.currentStateType = initialState;
|
||||||
|
this.activeState.enter(this.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(stateType: PlayerStateType) {
|
||||||
|
if (this.currentStateType === stateType) return;
|
||||||
|
|
||||||
|
console.log(`[PlayerState] ${this.currentStateType} -> ${stateType}`);
|
||||||
|
|
||||||
|
this.activeState.exit(this.context);
|
||||||
|
this.activeState = this.states.get(stateType)!;
|
||||||
|
this.currentStateType = stateType;
|
||||||
|
this.activeState.enter(this.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime: number) {
|
||||||
|
this.activeState.update(this.context, deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export enum PlayerStateType {
|
||||||
|
Idle = "idle",
|
||||||
|
Run = "run",
|
||||||
|
TurnToTarget = "turn_to_target",
|
||||||
|
Loot = "loot",
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/** idle/run -> turn (blend into facing pose before the swing). */
|
||||||
|
export const TURN_IDLE_FADE = 0.1;
|
||||||
|
/** turn -> loot (windup into the bat swing). */
|
||||||
|
export const LOOT_ENTER_FADE = 0.3;
|
||||||
|
/** loot/combat -> idle (settle back after combat). */
|
||||||
|
export const COMBAT_EXIT_FADE = 0.3;
|
||||||
|
/** any -> run (start moving). */
|
||||||
|
export const RUN_ENTER_FADE = 0.2;
|
||||||
|
|
||||||
|
/** Normalized clip time (0–1) when the bat crosses the target: R→L, then L→R. */
|
||||||
|
export const ATTACK_STRIKE_MARKS = [0.48, 0.72];
|
||||||
|
/** After 1st hit (R→L) — safe exit if combat ends before 2nd hit (L→R). */
|
||||||
|
export const LOOT_EXIT_AFTER_FIRST_STRIKE = 0.5;
|
||||||
|
/** After 2nd hit (L→R) — follow-through before idle. */
|
||||||
|
export const LOOT_EXIT_AFTER_SECOND_STRIKE = 0.78;
|
||||||
|
/** End of clip when exiting before any strike (e.g. left the zone). */
|
||||||
|
export const LOOT_EXIT_END_OF_CYCLE = 0.95;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { BaseAnimation } from "../../Enums/BaseAnimation";
|
||||||
|
import { IPlayerState } from "../IPlayerState";
|
||||||
|
import { PlayerContext } from "../PlayerContext";
|
||||||
|
import { PlayerStateType } from "../PlayerStateType";
|
||||||
|
|
||||||
|
export class IdleState implements IPlayerState {
|
||||||
|
enter(context: PlayerContext): void {
|
||||||
|
if (context.skipStateEnterAnimation) {
|
||||||
|
context.skipStateEnterAnimation = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.character.playAnimation(BaseAnimation.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(_context: PlayerContext): void {}
|
||||||
|
|
||||||
|
update(context: PlayerContext, _deltaTime: number): void {
|
||||||
|
if (context.isMoving()) {
|
||||||
|
context.stateMachine.setState(PlayerStateType.Run);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = context.physics.getPhysicsBody();
|
||||||
|
body.velocity.set(0, 0, 0);
|
||||||
|
body.wakeUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { BaseAnimation } from "../../Enums/BaseAnimation";
|
||||||
|
import {
|
||||||
|
ATTACK_STRIKE_MARKS,
|
||||||
|
COMBAT_EXIT_FADE,
|
||||||
|
LOOT_ENTER_FADE,
|
||||||
|
LOOT_EXIT_AFTER_FIRST_STRIKE,
|
||||||
|
LOOT_EXIT_AFTER_SECOND_STRIKE,
|
||||||
|
LOOT_EXIT_END_OF_CYCLE,
|
||||||
|
} from "../combatConstants";
|
||||||
|
import { IPlayerState } from "../IPlayerState";
|
||||||
|
import { PlayerContext } from "../PlayerContext";
|
||||||
|
import { PlayerStateType } from "../PlayerStateType";
|
||||||
|
|
||||||
|
export class LootState implements IPlayerState {
|
||||||
|
private strikeMarkIndex = 0;
|
||||||
|
private prevStrikeNormTime = 0;
|
||||||
|
|
||||||
|
enter(context: PlayerContext): void {
|
||||||
|
context.zeroVelocity();
|
||||||
|
|
||||||
|
context.character.setBatEquipped(true);
|
||||||
|
this.strikeMarkIndex = 0;
|
||||||
|
this.prevStrikeNormTime = 0;
|
||||||
|
|
||||||
|
const animId = this.getBatAttackAnimation(context);
|
||||||
|
context.character.crossFadeToAnimation(animId, false, LOOT_ENTER_FADE);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(_context: PlayerContext): void {}
|
||||||
|
|
||||||
|
update(context: PlayerContext, _deltaTime: number): void {
|
||||||
|
context.zeroVelocity();
|
||||||
|
|
||||||
|
context.rotation.setTargetWorldPosition(context.combatTargetWorldPosition);
|
||||||
|
|
||||||
|
const action = context.character.curClipAction;
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
const clipDuration = action.getClip().duration;
|
||||||
|
if (clipDuration <= 0) return;
|
||||||
|
|
||||||
|
const normalizedTime = action.time / clipDuration;
|
||||||
|
|
||||||
|
if (normalizedTime < this.prevStrikeNormTime) {
|
||||||
|
if (context.pendingCombatExit) {
|
||||||
|
this.finishCombatExit(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.strikeMarkIndex = 0;
|
||||||
|
}
|
||||||
|
this.prevStrikeNormTime = normalizedTime;
|
||||||
|
|
||||||
|
while (
|
||||||
|
this.strikeMarkIndex < ATTACK_STRIKE_MARKS.length &&
|
||||||
|
normalizedTime >= ATTACK_STRIKE_MARKS[this.strikeMarkIndex]
|
||||||
|
) {
|
||||||
|
context.onStrike?.();
|
||||||
|
this.strikeMarkIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.pendingCombatExit && this.canExitLootNow(normalizedTime)) {
|
||||||
|
this.finishCombatExit(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private canExitLootNow(normalizedTime: number) {
|
||||||
|
if (this.strikeMarkIndex >= 2) {
|
||||||
|
return normalizedTime >= LOOT_EXIT_AFTER_SECOND_STRIKE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.strikeMarkIndex >= 1) {
|
||||||
|
return normalizedTime >= LOOT_EXIT_AFTER_FIRST_STRIKE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedTime >= LOOT_EXIT_END_OF_CYCLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBatAttackAnimation(context: PlayerContext) {
|
||||||
|
if (context.character.animationList.length > BaseAnimation.Loot) {
|
||||||
|
return BaseAnimation.Loot;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BaseAnimation.Idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private finishCombatExit(context: PlayerContext) {
|
||||||
|
context.pendingCombatExit = false;
|
||||||
|
context.isAutoAttacking = false;
|
||||||
|
context.onStrike = null;
|
||||||
|
context.character.setDefaultWeapons();
|
||||||
|
context.syncRotation();
|
||||||
|
|
||||||
|
if (context.isMoving()) {
|
||||||
|
context.stateMachine.setState(PlayerStateType.Run);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.character.crossFadeToAnimation(BaseAnimation.Idle, false, COMBAT_EXIT_FADE);
|
||||||
|
context.skipStateEnterAnimation = true;
|
||||||
|
context.stateMachine.setState(PlayerStateType.Idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { BaseAnimation } from "../../Enums/BaseAnimation";
|
||||||
|
import { Vector3TToC } from "../../Helper";
|
||||||
|
import { FollowCameraC } from "../../Movment/CameraMovment/FollowCamera";
|
||||||
|
import { RUN_ENTER_FADE } from "../combatConstants";
|
||||||
|
import { IPlayerState } from "../IPlayerState";
|
||||||
|
import { PlayerContext } from "../PlayerContext";
|
||||||
|
import { PlayerStateType } from "../PlayerStateType";
|
||||||
|
|
||||||
|
/** Anim playback never drops below this, so legs don't freeze at tiny joystick tilts. */
|
||||||
|
const MIN_RUN_TIME_SCALE = 0.3;
|
||||||
|
/** How fast the run anim speed catches up to the joystick tilt. */
|
||||||
|
const RUN_TIME_SCALE_LERP = 10;
|
||||||
|
|
||||||
|
export class RunState implements IPlayerState {
|
||||||
|
private timeScale = MIN_RUN_TIME_SCALE;
|
||||||
|
|
||||||
|
enter(context: PlayerContext): void {
|
||||||
|
this.timeScale = Math.max(context.movement.Weight, MIN_RUN_TIME_SCALE);
|
||||||
|
if (context.character.isPlayingAnimation(BaseAnimation.Run)) {
|
||||||
|
context.character.setCurrentTimeScale(this.timeScale);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.character.crossFadeToAnimation(BaseAnimation.Run, false, RUN_ENTER_FADE);
|
||||||
|
context.character.setCurrentTimeScale(this.timeScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(context: PlayerContext): void {
|
||||||
|
context.character.setCurrentTimeScale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(context: PlayerContext, deltaTime: number): void {
|
||||||
|
const direction = context.movement.Direction;
|
||||||
|
|
||||||
|
if (direction.lengthSq() === 0) {
|
||||||
|
context.stateMachine.setState(PlayerStateType.Idle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FollowCameraC.inputDirection.copy(direction).normalize();
|
||||||
|
context.rotation.setTargetDirection(direction);
|
||||||
|
|
||||||
|
const body = context.physics.getPhysicsBody();
|
||||||
|
body.velocity.copy(Vector3TToC(direction));
|
||||||
|
body.wakeUp();
|
||||||
|
|
||||||
|
const targetScale = Math.max(context.movement.Weight, MIN_RUN_TIME_SCALE);
|
||||||
|
const t = Math.min(deltaTime * RUN_TIME_SCALE_LERP, 1);
|
||||||
|
this.timeScale += (targetScale - this.timeScale) * t;
|
||||||
|
context.character.setCurrentTimeScale(this.timeScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { BaseAnimation } from "../../Enums/BaseAnimation";
|
||||||
|
import { TURN_IDLE_FADE } from "../combatConstants";
|
||||||
|
import { IPlayerState } from "../IPlayerState";
|
||||||
|
import { PlayerContext } from "../PlayerContext";
|
||||||
|
import { PlayerStateType } from "../PlayerStateType";
|
||||||
|
|
||||||
|
export class TurnToTargetState implements IPlayerState {
|
||||||
|
enter(context: PlayerContext): void {
|
||||||
|
context.rotation.setTargetWorldPosition(context.combatTargetWorldPosition);
|
||||||
|
context.zeroVelocity();
|
||||||
|
context.character.playAnimation(BaseAnimation.Idle, false, TURN_IDLE_FADE);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(_context: PlayerContext): void {}
|
||||||
|
|
||||||
|
update(context: PlayerContext, _deltaTime: number): void {
|
||||||
|
context.zeroVelocity();
|
||||||
|
|
||||||
|
if (!context.rotation.isComplete()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.syncRotation();
|
||||||
|
context.stateMachine.setState(PlayerStateType.Loot);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,14 +9,19 @@ import { ResourceScreenFly } from "./ResourceScreenFly";
|
|||||||
import { ResourceType } from "./ResourceType";
|
import { ResourceType } from "./ResourceType";
|
||||||
import { ResourceUIC } from "./ResourceUIC";
|
import { ResourceUIC } from "./ResourceUIC";
|
||||||
|
|
||||||
|
/** Screen-space flight duration (icon → deposit zone). */
|
||||||
const DEPOSIT_FLY_MS = 520;
|
const DEPOSIT_FLY_MS = 520;
|
||||||
|
/** Pickup scale at the end of the flight (1 → this value). */
|
||||||
const DEPOSIT_FLY_SCALE = 0.35;
|
const DEPOSIT_FLY_SCALE = 0.35;
|
||||||
const CHAIN_DEPOSIT_DELAY_MS = 140;
|
/** Delay between starting each pickup in the chain (overlapping flights). */
|
||||||
|
const CHAIN_DEPOSIT_STAGGER_MS = 70;
|
||||||
|
|
||||||
export class ResourceDepositC {
|
export class ResourceDepositC {
|
||||||
private static inited = false;
|
private static inited = false;
|
||||||
private static tweenGroup = new Group();
|
private static tweenGroup = new Group();
|
||||||
private static isProcessing = false;
|
private static isDepositing = false;
|
||||||
|
private static activeFlights = 0;
|
||||||
|
private static pendingLaunches = 0;
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
if (this.inited) return;
|
if (this.inited) return;
|
||||||
@@ -40,8 +45,8 @@ export class ResourceDepositC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static tryStart() {
|
static tryStart() {
|
||||||
if (!this.canDeposit()) return;
|
if (this.isDepositing || !this.canDeposit()) return;
|
||||||
this.processNext();
|
this.startDepositChain();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static canDeposit() {
|
private static canDeposit() {
|
||||||
@@ -61,23 +66,55 @@ export class ResourceDepositC {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static processNext() {
|
private static getDepositableCount() {
|
||||||
if (this.isProcessing) return;
|
let total = 0;
|
||||||
|
for (const type of Object.values(ResourceType)) {
|
||||||
|
total += ResourceInventoryC.get(type);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
const type = this.getDepositableType();
|
private static startDepositChain() {
|
||||||
if (!type || !this.canDeposit()) {
|
this.isDepositing = true;
|
||||||
this.isProcessing = false;
|
this.pendingLaunches = this.getDepositableCount();
|
||||||
|
this.scheduleChainStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static scheduleChainStep() {
|
||||||
|
if (!this.isDepositing) return;
|
||||||
|
|
||||||
|
if (this.pendingLaunches <= 0 || !this.canDeposit()) {
|
||||||
|
this.tryFinishChain();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const type = this.getDepositableType();
|
||||||
|
if (!type) {
|
||||||
|
this.tryFinishChain();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingLaunches -= 1;
|
||||||
|
|
||||||
|
if (!this.launchOneDeposit(type)) {
|
||||||
|
this.tryFinishChain();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pendingLaunches > 0) {
|
||||||
|
window.setTimeout(() => this.scheduleChainStep(), CHAIN_DEPOSIT_STAGGER_MS);
|
||||||
|
} else {
|
||||||
|
this.tryFinishChain();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static launchOneDeposit(type: ResourceType) {
|
||||||
const iconCenter = ResourceUIC.getIconCenter(type);
|
const iconCenter = ResourceUIC.getIconCenter(type);
|
||||||
const zoneScreen = InteractiveZoneC.getScreenCenter();
|
const zoneScreen = InteractiveZoneC.getScreenCenter();
|
||||||
if (!iconCenter) return;
|
if (!iconCenter) return false;
|
||||||
|
|
||||||
const pickup = ResourceScreenFly.createPickup(type);
|
const pickup = ResourceScreenFly.createPickup(type);
|
||||||
if (!pickup) return;
|
if (!pickup) return false;
|
||||||
|
|
||||||
this.isProcessing = true;
|
|
||||||
|
|
||||||
const startScreen = ResourceScreenFly.screenPointFromClient(
|
const startScreen = ResourceScreenFly.screenPointFromClient(
|
||||||
iconCenter.x,
|
iconCenter.x,
|
||||||
@@ -85,6 +122,8 @@ export class ResourceDepositC {
|
|||||||
zoneScreen.z,
|
zoneScreen.z,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.activeFlights += 1;
|
||||||
|
|
||||||
ResourceScreenFly.flyAlongScreen(
|
ResourceScreenFly.flyAlongScreen(
|
||||||
pickup,
|
pickup,
|
||||||
startScreen,
|
startScreen,
|
||||||
@@ -97,11 +136,16 @@ export class ResourceDepositC {
|
|||||||
ResourceUIC.refresh(type);
|
ResourceUIC.refresh(type);
|
||||||
InteractiveZoneC.addDeposit(1);
|
InteractiveZoneC.addDeposit(1);
|
||||||
|
|
||||||
window.setTimeout(() => {
|
this.activeFlights -= 1;
|
||||||
this.isProcessing = false;
|
this.tryFinishChain();
|
||||||
this.processNext();
|
|
||||||
}, CHAIN_DEPOSIT_DELAY_MS);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static tryFinishChain() {
|
||||||
|
if (this.activeFlights > 0) return;
|
||||||
|
this.isDepositing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ const BOUNCE_HEIGHTS = [0.22, 0.09];
|
|||||||
const BOUNCE_UP_MS = 200;
|
const BOUNCE_UP_MS = 200;
|
||||||
const BOUNCE_DOWN_MS = 180;
|
const BOUNCE_DOWN_MS = 180;
|
||||||
const REST_AFTER_BOUNCE_MS = 400;
|
const REST_AFTER_BOUNCE_MS = 400;
|
||||||
|
/** Screen-space flight duration (world pickup → UI icon). */
|
||||||
const FLY_DURATION_MS = 520;
|
const FLY_DURATION_MS = 520;
|
||||||
const CHAIN_FLY_DELAY_MS = 140;
|
/** Pickup scale at the end of the flight (1 → this value). */
|
||||||
const FLY_SCALE = 0.35;
|
const FLY_SCALE = 0.35;
|
||||||
const STAGGER_MS = 70;
|
/** Delay between starting each fly-to-UI after bounce (overlapping flights). */
|
||||||
|
const CHAIN_FLY_STAGGER_MS = 70;
|
||||||
|
/** Delay between spawning each ground pickup from a loot burst. */
|
||||||
|
const SPAWN_STAGGER_MS = 70;
|
||||||
const GROUND_LIFT = 0.08;
|
const GROUND_LIFT = 0.08;
|
||||||
|
|
||||||
type FlyQueueItem = {
|
type FlyQueueItem = {
|
||||||
@@ -32,7 +36,7 @@ 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 flyQueue: FlyQueueItem[] = [];
|
||||||
private static isProcessingFlyQueue = false;
|
private static isDrainingFlyQueue = false;
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
if (this.inited) return;
|
if (this.inited) return;
|
||||||
@@ -58,7 +62,7 @@ export class ResourceFlyC {
|
|||||||
static launch(origin: Vector3, type: ResourceType, index: number, count: number) {
|
static launch(origin: Vector3, type: ResourceType, index: number, count: number) {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.startPickup(origin, type, index, count);
|
this.startPickup(origin, type, index, count);
|
||||||
}, index * STAGGER_MS);
|
}, index * SPAWN_STAGGER_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static startPickup(origin: Vector3, type: ResourceType, index: number, count: number) {
|
private static startPickup(origin: Vector3, type: ResourceType, index: number, count: number) {
|
||||||
@@ -153,32 +157,32 @@ export class ResourceFlyC {
|
|||||||
|
|
||||||
private static enqueueFlyToUI(pickup: Object3D, type: ResourceType) {
|
private static enqueueFlyToUI(pickup: Object3D, type: ResourceType) {
|
||||||
this.flyQueue.push({ pickup, type });
|
this.flyQueue.push({ pickup, type });
|
||||||
this.processFlyQueue();
|
if (!this.isDrainingFlyQueue) {
|
||||||
|
this.drainFlyChainStep();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static processFlyQueue() {
|
private static drainFlyChainStep() {
|
||||||
if (this.isProcessingFlyQueue || this.flyQueue.length === 0) return;
|
|
||||||
|
|
||||||
this.isProcessingFlyQueue = true;
|
|
||||||
const item = this.flyQueue.shift();
|
const item = this.flyQueue.shift();
|
||||||
if (!item) {
|
if (!item) {
|
||||||
this.isProcessingFlyQueue = false;
|
this.isDrainingFlyQueue = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.flyToUI(item.pickup, item.type, () => {
|
this.isDrainingFlyQueue = true;
|
||||||
window.setTimeout(() => {
|
this.flyToUI(item.pickup, item.type);
|
||||||
this.isProcessingFlyQueue = false;
|
|
||||||
this.processFlyQueue();
|
if (this.flyQueue.length > 0) {
|
||||||
}, CHAIN_FLY_DELAY_MS);
|
window.setTimeout(() => this.drainFlyChainStep(), CHAIN_FLY_STAGGER_MS);
|
||||||
});
|
} else {
|
||||||
|
this.isDrainingFlyQueue = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static flyToUI(pickup: Object3D, type: ResourceType, onComplete: () => void) {
|
private static flyToUI(pickup: Object3D, type: ResourceType) {
|
||||||
const iconCenter = ResourceUIC.getIconCenter(type);
|
const iconCenter = ResourceUIC.getIconCenter(type);
|
||||||
if (!iconCenter) {
|
if (!iconCenter) {
|
||||||
ThreeC.removeFromScene(pickup);
|
ThreeC.removeFromScene(pickup);
|
||||||
onComplete();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +203,6 @@ export class ResourceFlyC {
|
|||||||
() => {
|
() => {
|
||||||
ResourceInventoryC.add(type, 1);
|
ResourceInventoryC.add(type, 1);
|
||||||
ResourceUIC.refresh(type);
|
ResourceUIC.refresh(type);
|
||||||
onComplete();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,18 @@ canvas {
|
|||||||
z-index: 900;
|
z-index: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#joystick_zone .joystick .back {
|
||||||
|
background: transparent !important;
|
||||||
|
border: 0.35vh solid #ffffff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#joystick_zone .joystick .front {
|
||||||
|
background: rgba(255, 255, 255, 0.15) !important;
|
||||||
|
border: 0.2vh solid #ffffff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
@media (orientation: landscape) {
|
||||||
#videoPlayable {
|
#videoPlayable {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ui.is-pre-gameplay .hud-invasion-title,
|
#ui.is-pre-gameplay .hud-invasion-title,
|
||||||
#ui.is-pre-gameplay .hud-progress,
|
#ui.is-pre-gameplay .hud-progress,
|
||||||
#ui.is-pre-gameplay .hud-top-right {
|
#ui.is-pre-gameplay .hud-top-right,
|
||||||
|
#ui.is-pre-gameplay .hud-bottom-left {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,8 +66,8 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 0.35vh solid rgba(255, 255, 255, 0.55);
|
border: 0.35vh solid #ffffff;
|
||||||
background: rgba(255, 255, 255, 0.12);
|
background: transparent;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +80,9 @@
|
|||||||
margin-left: -4vh;
|
margin-left: -4vh;
|
||||||
margin-top: -4vh;
|
margin-top: -4vh;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #ffffff;
|
border: 0.2vh solid #ffffff;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
box-sizing: border-box;
|
||||||
animation: pre-gameplay-joystick-knob 2.4s ease-in-out infinite;
|
animation: pre-gameplay-joystick-knob 2.4s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user