From 930ed00c952b37632b0cea9cb0aca79b48ee1fd7 Mon Sep 17 00:00:00 2001 From: Vasyl Kazakov Date: Wed, 17 Jun 2026 17:06:13 +0300 Subject: [PATCH] fixed bugs --- src/configUIParams/globalSettings.ts | 2 +- src/controllers/Map/GatherC.ts | 2 - src/controllers/Map/PhysicsTriggerC.ts | 7 +- src/controllers/Map/PropHpBar.ts | 21 +- .../Presets/Character/Character.ts | 114 +++++++- src/controllers/Presets/Player.ts | 156 +++++----- src/controllers/Presets/PlayerCombat.ts | 273 ------------------ .../Presets/PlayerState/IPlayerState.ts | 7 + .../Presets/PlayerState/PlayerContext.ts | 49 ++++ .../Presets/PlayerState/PlayerStateMachine.ts | 39 +++ .../Presets/PlayerState/PlayerStateType.ts | 6 + .../Presets/PlayerState/combatConstants.ts | 17 ++ .../Presets/PlayerState/states/IdleState.ts | 28 ++ .../Presets/PlayerState/states/LootState.ts | 102 +++++++ .../Presets/PlayerState/states/RunState.ts | 51 ++++ .../PlayerState/states/TurnToTargetState.ts | 26 ++ src/controllers/Resources/ResourceDepositC.ts | 78 +++-- src/controllers/Resources/ResourceFlyC.ts | 41 +-- src/css/main.css | 12 + src/css/preGameplay.css | 11 +- 20 files changed, 627 insertions(+), 415 deletions(-) delete mode 100644 src/controllers/Presets/PlayerCombat.ts create mode 100644 src/controllers/Presets/PlayerState/IPlayerState.ts create mode 100644 src/controllers/Presets/PlayerState/PlayerContext.ts create mode 100644 src/controllers/Presets/PlayerState/PlayerStateMachine.ts create mode 100644 src/controllers/Presets/PlayerState/PlayerStateType.ts create mode 100644 src/controllers/Presets/PlayerState/combatConstants.ts create mode 100644 src/controllers/Presets/PlayerState/states/IdleState.ts create mode 100644 src/controllers/Presets/PlayerState/states/LootState.ts create mode 100644 src/controllers/Presets/PlayerState/states/RunState.ts create mode 100644 src/controllers/Presets/PlayerState/states/TurnToTargetState.ts diff --git a/src/configUIParams/globalSettings.ts b/src/configUIParams/globalSettings.ts index 25e2cfc..1c57233 100644 --- a/src/configUIParams/globalSettings.ts +++ b/src/configUIParams/globalSettings.ts @@ -67,7 +67,7 @@ export const globalSettings: ConfigUiParamsCategories[] = [ [ -360, 360, - 0 + 2 ], [ -360, diff --git a/src/controllers/Map/GatherC.ts b/src/controllers/Map/GatherC.ts index 1b217d1..0c0c1bf 100644 --- a/src/controllers/Map/GatherC.ts +++ b/src/controllers/Map/GatherC.ts @@ -2,7 +2,6 @@ import { EasyEvent, JoystickC, UpdateController } from "@24tools/playable_templa import { Vector3 } from "three"; import { PhysicsTriggerC } from "./PhysicsTriggerC"; import { PropC, PropRegistry } from "./PropC"; -import { PropHpUIC } from "./PropHpUIC"; import { Player } from "../Presets/Player"; export class GatherC { @@ -120,7 +119,6 @@ export class GatherC { if (Player.isMoving() || Player.isCombatBusy()) return; const targetPosition = this.getTargetPosition(props); - PropHpUIC.showFor(props); Player.startAutoAttack( () => this.onAttackStrike(), () => {}, diff --git a/src/controllers/Map/PhysicsTriggerC.ts b/src/controllers/Map/PhysicsTriggerC.ts index 75ddd49..452f01f 100644 --- a/src/controllers/Map/PhysicsTriggerC.ts +++ b/src/controllers/Map/PhysicsTriggerC.ts @@ -5,7 +5,7 @@ import { import { Box3, Object3D, Vector3 } from "three"; import { Body } from "cannon-es"; -const PLAYER_RADIUS = 0.3; +const PLAYER_RADIUS = 0.5; export type TriggerEventPayload = { lootableObject: Object3D; @@ -53,7 +53,7 @@ export class PhysicsTriggerC { lootableObject, triggerObject, 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) { 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 distanceSq = dx * dx + dy * dy + dz * dz; + const distanceSq = dx * dx + dz * dz; const isInside = distanceSq <= (trigger.radius + PLAYER_RADIUS) ** 2; const wasInside = this.activeTriggers.has(trigger); diff --git a/src/controllers/Map/PropHpBar.ts b/src/controllers/Map/PropHpBar.ts index a81ffba..19948e5 100644 --- a/src/controllers/Map/PropHpBar.ts +++ b/src/controllers/Map/PropHpBar.ts @@ -38,8 +38,8 @@ type FillPart = { node: Object3D; baseScaleX: number; basePosX: number; - /** Local mesh min.x (negative); keeps left edge fixed when scaling. */ - anchorMinX: number; + /** Local mesh max.x; keeps right edge fixed when scaling. */ + anchorMaxX: number; }; export class PropHpBar { @@ -59,6 +59,7 @@ export class PropHpBar { private displayedHealth: number; private visible = false; + private hasTakenDamage = false; static getPrefab(): Object3D | null { const fromScene = ThreeC.getObject(HP_BAR_MESH); @@ -108,6 +109,8 @@ export class PropHpBar { } onDamage() { + this.hasTakenDamage = true; + if (!this.visible) { this.setVisible(true); return; @@ -137,6 +140,9 @@ export class PropHpBar { } 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.root.visible = value; @@ -164,11 +170,10 @@ export class PropHpBar { const clamped = Math.max(0, Math.min(1, ratio)); part.node.scale.x = part.baseScaleX * clamped; part.node.position.x = - part.basePosX + part.anchorMinX * part.baseScaleX * (1 - clamped); + part.basePosX + part.anchorMaxX * part.baseScaleX * (1 - clamped); } private createFillPart(node: Object3D): FillPart { - const meshMinX = this.getMeshMinX(node); const isForeground = node.name.includes(NODE_FOREGROUND); node.renderOrder = isForeground @@ -179,16 +184,16 @@ export class PropHpBar { node, baseScaleX: node.scale.x, basePosX: node.position.x, - anchorMinX: meshMinX, + anchorMaxX: this.getMeshMaxX(node), }; } - private getMeshMinX(node: Object3D) { + private getMeshMaxX(node: Object3D) { const mesh = node as Mesh; - if (!mesh.isMesh || !mesh.geometry) return -0.935; + if (!mesh.isMesh || !mesh.geometry) return 0; mesh.geometry.computeBoundingBox(); - return mesh.geometry.boundingBox?.min.x ?? -0.935; + return mesh.geometry.boundingBox?.max.x ?? 0; } private getHealthRatio(health: number) { diff --git a/src/controllers/Presets/Character/Character.ts b/src/controllers/Presets/Character/Character.ts index 648c987..ec37ed8 100644 --- a/src/controllers/Presets/Character/Character.ts +++ b/src/controllers/Presets/Character/Character.ts @@ -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 { clone } from "three/examples/jsm/utils/SkeletonUtils"; import { ThreeC } from "../../ThreeC"; 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 { tObj: Object3D; @@ -14,6 +20,14 @@ export class Character { onAnimLoop: 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 | null = null; + private tool2Tween: Tween | null = null; + constructor(prefab: GLTF, start_position = new Vector3()) { const tObj = clone(prefab.scene); @@ -35,18 +49,35 @@ export class Character { ThreeC.addAnimMixer(animMixer); + this.cacheWeapons(); + UpdateController.Instance.onUpdate.addDelegate(() => { + this.weaponTweens.update(performance.now()); + }); + return this; } - set AnimationSpeed(timeScale: number) { - if (this.curClipAction) { - this.curClipAction.timeScale = timeScale; - } - } + private cacheWeapons() { + this.tObj.traverse((child) => { + 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) { - if (this.curClipAction) { - this.curClipAction.weight = weight; + // Resting state: bat sits on the back (Tool_2), hand tool (Tool_1) hidden. + // Snapping here avoids a startup pop when setDefaultWeapons runs on init. + 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() { - this.setObjectVisible("Tool_1", false); - this.setObjectVisible("Tool_2", true); 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) { - this.setObjectVisible("Tool_1", equipped); - this.setObjectVisible("Tool_2", !equipped); 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 | null, + ): Tween | 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) { diff --git a/src/controllers/Presets/Player.ts b/src/controllers/Presets/Player.ts index 28cdf72..e907bd9 100644 --- a/src/controllers/Presets/Player.ts +++ b/src/controllers/Presets/Player.ts @@ -9,18 +9,26 @@ import { Object3D, Vector3 } from "three"; import { ThreeC } from "../ThreeC"; import { PlayerInput } from "./Input/PlayerInput"; import { MoveC } from "./Movment/MoveC"; -import { Vector3CToT, Vector3TToC } from "./Helper"; +import { Vector3CToT } from "./Helper"; import { RotationC } from "./Movment/RotationC"; import { FollowCameraC } from "./Movment/CameraMovment/FollowCamera"; -import { PlayerCombat } from "./PlayerCombat"; import { PhysicsTriggerC } from "../Map/PhysicsTriggerC"; 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 { private static inited = false; - private static isRunning = false; private static updateDelegate: Delegate; + private static context: PlayerContext; + private static stateMachine: PlayerStateMachine; private static container: Object3D = new Object3D(); private static input: PlayerInput; @@ -40,7 +48,6 @@ export class Player { this.container.position.copy(this.spawnPosition); this.character.setDefaultWeapons(); - this.character.playAnimation(BaseAnimation.Idle); this.container.add(this.character.tObj); ThreeC.addToScene(this.container); @@ -53,24 +60,35 @@ export class Player { this.movement = new MoveC(this.input, moveSpeed); this.rotation = new RotationC(this.container, rotationSpeed); - PlayerCombat.init({ + this.context = new PlayerContext({ character: this.character, - getPhysicsBody: () => this.physics.getPhysicsBody(), - isMoving: () => this.isMoving(), - onStopRunning: () => this.stopRunning(), - onResumeRunning: () => this.resumeRunning(), - onSetTurnTarget: (worldPosition) => this.rotation.setTargetWorldPosition(worldPosition), - onUpdateRotation: (delta) => this.rotation.update(delta), - isRotationComplete: () => this.rotation.isComplete(), - onSyncRotation: () => this.syncRotationToFacing(), + movement: this.movement, + rotation: this.rotation, + physics: this.physics, + container: this.container, }); + this.stateMachine = new PlayerStateMachine( + this.context, + new Map([ + [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((delta) => this.update(delta)); UpdateController.Instance.onUpdate.addListener(this.updateDelegate); FollowCameraC.init(this.container); } + static get currentState(): PlayerStateType { + return this.stateMachine.currentState; + } + static getWorldPosition() { return this.container.getWorldPosition(new Vector3()); } @@ -79,113 +97,103 @@ export class Player { return this.movement.Direction.lengthSq() > 0; } + static isInCombat() { + const state = this.currentState; + return state === PlayerStateType.TurnToTarget || state === PlayerStateType.Loot; + } + static isCombatBusy() { - return PlayerCombat.isBusy; + return this.isInCombat() || this.context.pendingCombatExit; } static isAutoAttackActive() { - return PlayerCombat.isAutoAttackActive; + return this.context.isAutoAttacking; } static startAutoAttack( onStrike: () => void, - onComplete: () => void, + _onComplete: () => void, 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() { - 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(); } 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) { - PlayerCombat.playAttack(onStrike); + this.context.onStrike = onStrike; + this.stateMachine.setState(PlayerStateType.Loot); } private static initPhysics() { + const playerColliderRadius = 0.2; this.physics = new PhysicsBody( this.container, false, 1, PhysicsLayer.Player, PhysicsLayer.Wall | PhysicsLayer.Trigger, + playerColliderRadius, ); } - private static startRunning() { - if (PlayerCombat.isBusy) return; - if (this.isRunning) return; - if (this.character.isPlayingAnimation(BaseAnimation.Run)) { - this.isRunning = true; + private static finishCombatExit() { + this.context.pendingCombatExit = false; + this.context.character.setDefaultWeapons(); + this.context.syncRotation(); + + if (this.isMoving()) { + this.stateMachine.setState(PlayerStateType.Run); return; } - this.character.playAnimation(BaseAnimation.Run); - this.isRunning = true; - } - private static resumeRunning() { - if (PlayerCombat.isBusy) return; - this.isRunning = true; - if (this.character.isPlayingAnimation(BaseAnimation.Run)) return; - this.character.crossFadeToAnimation(BaseAnimation.Run, false, 0.15); - } - - private static stopRunning() { - if (!this.isRunning) return; - if (!PlayerCombat.isBusy) { - this.character.playAnimation(BaseAnimation.Idle); - } - this.animationValue = 1; - this.isRunning = false; - } - - private static set animationValue(value: number) { - this.character.AnimationSpeed = value; - this.character.AnimationWeight = value * 12.5 + 87.5; + this.context.character.crossFadeToAnimation(BaseAnimation.Idle, false, COMBAT_EXIT_FADE); + this.context.skipStateEnterAnimation = true; + this.stateMachine.setState(PlayerStateType.Idle); } private static update(delta: number) { this.movement.update(delta); - if (this.isMoving()) { - PlayerCombat.cancelOnMoveInput(); - } - - 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(); + if (this.isMoving() && (this.context.isAutoAttacking || this.currentState === PlayerStateType.TurnToTarget)) { + this.stopAutoAttack(); } + this.stateMachine.update(delta); this.rotation.update(delta); - - this.physics.getPhysicsBody().velocity.copy(Vector3TToC(direction)); - this.physics.getPhysicsBody().wakeUp(); this.syncVisual(delta); } - private static syncRotationToFacing() { - this.rotation.syncToCurrentFacing(); - } - private static syncVisual(delta: number) { const lerpSpeed = 10; const targetPos = Vector3CToT(this.physics.getPhysicsBody().position); diff --git a/src/controllers/Presets/PlayerCombat.ts b/src/controllers/Presets/PlayerCombat.ts deleted file mode 100644 index 5efc974..0000000 --- a/src/controllers/Presets/PlayerCombat.ts +++ /dev/null @@ -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(); - } -} diff --git a/src/controllers/Presets/PlayerState/IPlayerState.ts b/src/controllers/Presets/PlayerState/IPlayerState.ts new file mode 100644 index 0000000..996aaca --- /dev/null +++ b/src/controllers/Presets/PlayerState/IPlayerState.ts @@ -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; +} diff --git a/src/controllers/Presets/PlayerState/PlayerContext.ts b/src/controllers/Presets/PlayerState/PlayerContext.ts new file mode 100644 index 0000000..54b4325 --- /dev/null +++ b/src/controllers/Presets/PlayerState/PlayerContext.ts @@ -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(); + } +} diff --git a/src/controllers/Presets/PlayerState/PlayerStateMachine.ts b/src/controllers/Presets/PlayerState/PlayerStateMachine.ts new file mode 100644 index 0000000..907ad72 --- /dev/null +++ b/src/controllers/Presets/PlayerState/PlayerStateMachine.ts @@ -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; + + get currentState(): PlayerStateType { + return this.currentStateType; + } + + constructor( + private readonly context: PlayerContext, + states: Map, + 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); + } +} diff --git a/src/controllers/Presets/PlayerState/PlayerStateType.ts b/src/controllers/Presets/PlayerState/PlayerStateType.ts new file mode 100644 index 0000000..bfccaf8 --- /dev/null +++ b/src/controllers/Presets/PlayerState/PlayerStateType.ts @@ -0,0 +1,6 @@ +export enum PlayerStateType { + Idle = "idle", + Run = "run", + TurnToTarget = "turn_to_target", + Loot = "loot", +} diff --git a/src/controllers/Presets/PlayerState/combatConstants.ts b/src/controllers/Presets/PlayerState/combatConstants.ts new file mode 100644 index 0000000..d57013e --- /dev/null +++ b/src/controllers/Presets/PlayerState/combatConstants.ts @@ -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; diff --git a/src/controllers/Presets/PlayerState/states/IdleState.ts b/src/controllers/Presets/PlayerState/states/IdleState.ts new file mode 100644 index 0000000..1b1979d --- /dev/null +++ b/src/controllers/Presets/PlayerState/states/IdleState.ts @@ -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(); + } +} diff --git a/src/controllers/Presets/PlayerState/states/LootState.ts b/src/controllers/Presets/PlayerState/states/LootState.ts new file mode 100644 index 0000000..6007c9a --- /dev/null +++ b/src/controllers/Presets/PlayerState/states/LootState.ts @@ -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); + } +} diff --git a/src/controllers/Presets/PlayerState/states/RunState.ts b/src/controllers/Presets/PlayerState/states/RunState.ts new file mode 100644 index 0000000..ec003a7 --- /dev/null +++ b/src/controllers/Presets/PlayerState/states/RunState.ts @@ -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); + } +} diff --git a/src/controllers/Presets/PlayerState/states/TurnToTargetState.ts b/src/controllers/Presets/PlayerState/states/TurnToTargetState.ts new file mode 100644 index 0000000..23cc3e6 --- /dev/null +++ b/src/controllers/Presets/PlayerState/states/TurnToTargetState.ts @@ -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); + } +} diff --git a/src/controllers/Resources/ResourceDepositC.ts b/src/controllers/Resources/ResourceDepositC.ts index 89fdcb4..9c0845d 100644 --- a/src/controllers/Resources/ResourceDepositC.ts +++ b/src/controllers/Resources/ResourceDepositC.ts @@ -9,14 +9,19 @@ import { ResourceScreenFly } from "./ResourceScreenFly"; import { ResourceType } from "./ResourceType"; import { ResourceUIC } from "./ResourceUIC"; +/** Screen-space flight duration (icon → deposit zone). */ const DEPOSIT_FLY_MS = 520; +/** Pickup scale at the end of the flight (1 → this value). */ 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 { private static inited = false; private static tweenGroup = new Group(); - private static isProcessing = false; + private static isDepositing = false; + private static activeFlights = 0; + private static pendingLaunches = 0; static init() { if (this.inited) return; @@ -40,8 +45,8 @@ export class ResourceDepositC { } static tryStart() { - if (!this.canDeposit()) return; - this.processNext(); + if (this.isDepositing || !this.canDeposit()) return; + this.startDepositChain(); } private static canDeposit() { @@ -61,23 +66,55 @@ export class ResourceDepositC { return null; } - private static processNext() { - if (this.isProcessing) return; + private static getDepositableCount() { + let total = 0; + for (const type of Object.values(ResourceType)) { + total += ResourceInventoryC.get(type); + } + return total; + } - const type = this.getDepositableType(); - if (!type || !this.canDeposit()) { - this.isProcessing = false; + private static startDepositChain() { + this.isDepositing = true; + this.pendingLaunches = this.getDepositableCount(); + this.scheduleChainStep(); + } + + private static scheduleChainStep() { + if (!this.isDepositing) return; + + if (this.pendingLaunches <= 0 || !this.canDeposit()) { + this.tryFinishChain(); 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 zoneScreen = InteractiveZoneC.getScreenCenter(); - if (!iconCenter) return; + if (!iconCenter) return false; const pickup = ResourceScreenFly.createPickup(type); - if (!pickup) return; - - this.isProcessing = true; + if (!pickup) return false; const startScreen = ResourceScreenFly.screenPointFromClient( iconCenter.x, @@ -85,6 +122,8 @@ export class ResourceDepositC { zoneScreen.z, ); + this.activeFlights += 1; + ResourceScreenFly.flyAlongScreen( pickup, startScreen, @@ -97,11 +136,16 @@ export class ResourceDepositC { ResourceUIC.refresh(type); InteractiveZoneC.addDeposit(1); - window.setTimeout(() => { - this.isProcessing = false; - this.processNext(); - }, CHAIN_DEPOSIT_DELAY_MS); + this.activeFlights -= 1; + this.tryFinishChain(); }, ); + + return true; + } + + private static tryFinishChain() { + if (this.activeFlights > 0) return; + this.isDepositing = false; } } diff --git a/src/controllers/Resources/ResourceFlyC.ts b/src/controllers/Resources/ResourceFlyC.ts index b577da2..61fed45 100644 --- a/src/controllers/Resources/ResourceFlyC.ts +++ b/src/controllers/Resources/ResourceFlyC.ts @@ -16,10 +16,14 @@ const BOUNCE_HEIGHTS = [0.22, 0.09]; const BOUNCE_UP_MS = 200; const BOUNCE_DOWN_MS = 180; const REST_AFTER_BOUNCE_MS = 400; +/** Screen-space flight duration (world pickup → UI icon). */ 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 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; type FlyQueueItem = { @@ -32,7 +36,7 @@ export class ResourceFlyC { private static inited = false; private static tweenGroup = new Group(); private static flyQueue: FlyQueueItem[] = []; - private static isProcessingFlyQueue = false; + private static isDrainingFlyQueue = false; static init() { if (this.inited) return; @@ -58,7 +62,7 @@ export class ResourceFlyC { static launch(origin: Vector3, type: ResourceType, index: number, count: number) { window.setTimeout(() => { this.startPickup(origin, type, index, count); - }, index * STAGGER_MS); + }, index * SPAWN_STAGGER_MS); } 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) { this.flyQueue.push({ pickup, type }); - this.processFlyQueue(); + if (!this.isDrainingFlyQueue) { + this.drainFlyChainStep(); + } } - private static processFlyQueue() { - if (this.isProcessingFlyQueue || this.flyQueue.length === 0) return; - - this.isProcessingFlyQueue = true; + private static drainFlyChainStep() { const item = this.flyQueue.shift(); if (!item) { - this.isProcessingFlyQueue = false; + this.isDrainingFlyQueue = false; return; } - this.flyToUI(item.pickup, item.type, () => { - window.setTimeout(() => { - this.isProcessingFlyQueue = false; - this.processFlyQueue(); - }, CHAIN_FLY_DELAY_MS); - }); + this.isDrainingFlyQueue = true; + this.flyToUI(item.pickup, item.type); + + if (this.flyQueue.length > 0) { + 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); if (!iconCenter) { ThreeC.removeFromScene(pickup); - onComplete(); return; } @@ -199,7 +203,6 @@ export class ResourceFlyC { () => { ResourceInventoryC.add(type, 1); ResourceUIC.refresh(type); - onComplete(); }, ); } diff --git a/src/css/main.css b/src/css/main.css index c0d73bb..52e25a2 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -129,6 +129,18 @@ canvas { 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) { #videoPlayable { height: 100vh; diff --git a/src/css/preGameplay.css b/src/css/preGameplay.css index 0c81e0b..0c00570 100644 --- a/src/css/preGameplay.css +++ b/src/css/preGameplay.css @@ -1,6 +1,7 @@ #ui.is-pre-gameplay .hud-invasion-title, #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; } @@ -65,8 +66,8 @@ position: absolute; inset: 0; border-radius: 50%; - border: 0.35vh solid rgba(255, 255, 255, 0.55); - background: rgba(255, 255, 255, 0.12); + border: 0.35vh solid #ffffff; + background: transparent; box-sizing: border-box; } @@ -79,7 +80,9 @@ margin-left: -4vh; margin-top: -4vh; 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; }