mirror of
https://github.com/solcloud/Counter-Strike.git
synced 2025-04-25 10:03:12 +02:00
Add scope mode for AWP
This commit is contained in:
parent
83ab7ed84c
commit
636bd9f19b
@ -15,6 +15,7 @@ abstract class Item
|
||||
private int $skinId;
|
||||
protected bool $equipped = false;
|
||||
protected int $price = 9999;
|
||||
protected int $scopeLevel = 0;
|
||||
private ?EquipEvent $eventEquip = null;
|
||||
/** @var array<string,int> */
|
||||
public readonly array $toArrayCache;
|
||||
@ -64,6 +65,11 @@ abstract class Item
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function getScopeLevel(): int
|
||||
{
|
||||
return $this->scopeLevel;
|
||||
}
|
||||
|
||||
public function decrementQuantity(): void
|
||||
{
|
||||
// empty hook
|
||||
@ -119,6 +125,7 @@ abstract class Item
|
||||
if ($this->eventEquip === null) {
|
||||
$this->eventEquip = new EquipEvent(function () {
|
||||
$this->equipped = true;
|
||||
$this->scopeLevel = 0;
|
||||
}, static::equipReadyTimeMs);
|
||||
}
|
||||
|
||||
@ -129,6 +136,7 @@ abstract class Item
|
||||
public function unEquip(): void
|
||||
{
|
||||
$this->equipped = false;
|
||||
$this->scopeLevel = 0;
|
||||
}
|
||||
|
||||
public function isEquipped(): bool
|
||||
|
@ -326,6 +326,7 @@ final class Player
|
||||
'ammo' => $ammo,
|
||||
'ammoReserve' => $ammoReserve,
|
||||
'isReloading' => $reloading,
|
||||
'scopeLevel' => $equippedItem->getScopeLevel(),
|
||||
];
|
||||
}
|
||||
|
||||
|
12
server/src/Interface/ScopeItem.php
Normal file
12
server/src/Interface/ScopeItem.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace cs\Interface;
|
||||
|
||||
interface ScopeItem
|
||||
{
|
||||
|
||||
public function scope(): void;
|
||||
|
||||
public function isScopedIn(): bool;
|
||||
|
||||
}
|
@ -14,6 +14,7 @@ use cs\Event\ThrowEvent;
|
||||
use cs\Interface\Attackable;
|
||||
use cs\Interface\AttackEnable;
|
||||
use cs\Interface\Reloadable;
|
||||
use cs\Interface\ScopeItem;
|
||||
use cs\Weapon\AmmoBasedWeapon;
|
||||
use cs\Weapon\Knife;
|
||||
|
||||
@ -57,6 +58,9 @@ trait AttackTrait
|
||||
public function attackSecondary(): ?AttackResult
|
||||
{
|
||||
$item = $this->getEquippedItem();
|
||||
if ($item instanceof ScopeItem) {
|
||||
$item->scope();
|
||||
}
|
||||
if (!($item instanceof AttackEnable)) {
|
||||
return null; // @codeCoverageIgnore
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ use cs\Enum\ItemType;
|
||||
use cs\Enum\SoundType;
|
||||
use cs\Event\PlayerMovementEvent;
|
||||
use cs\Event\SoundEvent;
|
||||
use cs\Interface\ScopeItem;
|
||||
|
||||
trait MovementTrait
|
||||
{
|
||||
@ -168,6 +169,9 @@ trait MovementTrait
|
||||
} elseif ($equippedItem->getType() === ItemType::TYPE_WEAPON_SECONDARY) {
|
||||
$speed *= Setting::getWeaponSecondarySpeedMultiplier($equippedItem->getId());
|
||||
}
|
||||
if ($equippedItem instanceof ScopeItem && $equippedItem->isScopedIn()) {
|
||||
$speed *= .5;
|
||||
}
|
||||
if ($this->isJumping()) {
|
||||
$speed *= Setting::jumpMovementSpeedMultiplier();
|
||||
} elseif ($this->isFlying()) {
|
||||
|
@ -71,9 +71,18 @@ abstract class AmmoBasedWeapon extends BaseWeapon implements Reloadable, AttackE
|
||||
$this->lastAttackTick = $event->getTickId();
|
||||
|
||||
$this->recoilModifier($event);
|
||||
$event->applyRecoil(...$this->getSpreadOffsets());
|
||||
return $event->fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float[] [offsetHorizontal, offsetVertical]
|
||||
*/
|
||||
protected function getSpreadOffsets(): array
|
||||
{
|
||||
return [0.0, 0.0];
|
||||
}
|
||||
|
||||
protected function resetRecoil(int $tickId = 0): void
|
||||
{
|
||||
$this->lastRecoilTick = $tickId;
|
||||
|
@ -4,8 +4,9 @@ namespace cs\Weapon;
|
||||
|
||||
use cs\Enum\ArmorType;
|
||||
use cs\Enum\HitBoxType;
|
||||
use cs\Interface\ScopeItem;
|
||||
|
||||
class RifleAWP extends AmmoBasedWeapon
|
||||
class RifleAWP extends AmmoBasedWeapon implements ScopeItem
|
||||
{
|
||||
|
||||
public const reloadTimeMs = 3700;
|
||||
@ -31,4 +32,27 @@ class RifleAWP extends AmmoBasedWeapon
|
||||
};
|
||||
}
|
||||
|
||||
protected function getSpreadOffsets(): array
|
||||
{
|
||||
if ($this->scopeLevel === 0) {
|
||||
return [rand(50, 120) / (rand(0, 1) === 0 ? -10 : +10), rand(40, 80) / (rand(0, 1) === 0 ? +10 : -10)];
|
||||
}
|
||||
|
||||
$this->scopeLevel = 0;
|
||||
return [0.0, 0.0];
|
||||
}
|
||||
|
||||
public function scope(): void
|
||||
{
|
||||
$this->scopeLevel++;
|
||||
if ($this->scopeLevel > 2) {
|
||||
$this->scopeLevel = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public function isScopedIn(): bool
|
||||
{
|
||||
return ($this->scopeLevel !== 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -105,6 +105,7 @@ class ProtocolTest extends BaseTest
|
||||
'ammo' => 12,
|
||||
'ammoReserve' => 120,
|
||||
'isReloading' => false,
|
||||
'scopeLevel' => 0,
|
||||
];
|
||||
$this->assertSame($playerSerializedExpected, $player->serialize());
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
--color-opponent: #de6868;
|
||||
--color-opponent-hsl: 0, 64%, 64%;
|
||||
--flash-bang-color: #FFFFFF;
|
||||
--scope-size: 2px;
|
||||
}
|
||||
|
||||
#hud .color-me {
|
||||
@ -19,11 +20,56 @@
|
||||
color: var(--color-opponent);
|
||||
}
|
||||
|
||||
#hud-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#hud #cross {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#hud #scope {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 82%;
|
||||
width: auto;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
margin: auto auto;
|
||||
box-shadow: 0 0 0 9999px #000, rgba(0, 0, 0, 0.5) 0 0 10px 4px inset;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#hud #scope .scope-cross {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
#hud #scope .scope-cross::before,
|
||||
#hud #scope .scope-cross::after {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
content: "";
|
||||
}
|
||||
|
||||
#hud #scope .scope-cross::before {
|
||||
border-left: var(--scope-size) solid #000000;
|
||||
transform: translate(calc(50% - 0.5 * var(--scope-size)), 0%);
|
||||
}
|
||||
|
||||
#hud #scope .scope-cross::after {
|
||||
border-top: var(--scope-size) solid #000000;
|
||||
transform: translate(0%, calc(-50% - 0.5 * var(--scope-size)));
|
||||
}
|
||||
|
||||
#hud #flash {
|
||||
|
@ -8,6 +8,7 @@ export class Game {
|
||||
#hud
|
||||
#stats
|
||||
#pointer
|
||||
#setting
|
||||
#shouldRenderInsideTick
|
||||
#tick = 0
|
||||
#round = 1
|
||||
@ -467,6 +468,9 @@ export class Game {
|
||||
player.get3DObject().getObjectByName('sight').position.y = serverState.sight
|
||||
player.get3DObject().position.set(serverState.position.x, serverState.position.y, -serverState.position.z)
|
||||
|
||||
if (player.data.scopeLevel !== serverState.scopeLevel) {
|
||||
this.#updateScopeState(player, serverState.scopeLevel)
|
||||
}
|
||||
if (player.data.isAttacker === this.playerMe.data.isAttacker) { // if player on my team
|
||||
if (player.data.money !== serverState.money) {
|
||||
this.#hud.updateMyTeamPlayerMoney(player.data, serverState.money)
|
||||
@ -476,6 +480,7 @@ export class Game {
|
||||
player.data.item = serverState.item
|
||||
player.data.sight = serverState.sight
|
||||
player.data.isAttacker = serverState.isAttacker
|
||||
player.data.scopeLevel = serverState.scopeLevel
|
||||
}
|
||||
|
||||
if (this.playerMe.getId() === serverState.id || this.playerSpectate.getId() === serverState.id) {
|
||||
@ -544,6 +549,20 @@ export class Game {
|
||||
modelInHand.visible = true
|
||||
}
|
||||
|
||||
#updateScopeState(player, scopeLevel) {
|
||||
const isPlayerSpectate = (this.playerSpectate.getId() === player.getId())
|
||||
if (scopeLevel > 0) {
|
||||
this.#world.playSound('210018__supakid13__sniper-scope-zoom-in.wav', player.data.position, isPlayerSpectate)
|
||||
}
|
||||
if (isPlayerSpectate) {
|
||||
this.#hud.updateCrossHair(scopeLevel)
|
||||
this.#world.updateCameraZoom(scopeLevel === 0 ? 1.0 : scopeLevel * 2.2)
|
||||
if (this.meIsAlive()) {
|
||||
this.#pointer.pointerSpeed = (scopeLevel === 0 ? this.#setting.getSensitivity() : this.#setting.getInScopeSensitivity())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getMyTeamPlayers() {
|
||||
let meIsAttacker = this.playerMe.isAttacker()
|
||||
return this.players.filter((player) => player.isAttacker() === meIsAttacker)
|
||||
@ -557,9 +576,10 @@ export class Game {
|
||||
return (!this.meIsAlive())
|
||||
}
|
||||
|
||||
setDependency(pointer, renderWorldInsideTick) {
|
||||
setDependency(pointer, setting) {
|
||||
this.#pointer = pointer
|
||||
this.#shouldRenderInsideTick = renderWorldInsideTick
|
||||
this.#setting = setting
|
||||
this.#shouldRenderInsideTick = setting.shouldMatchServerFps()
|
||||
}
|
||||
|
||||
getPlayerMeRotation() {
|
||||
|
@ -43,6 +43,8 @@ export class HUD {
|
||||
aliveOpponentTeam: null,
|
||||
time: null,
|
||||
killFeed: null,
|
||||
cross: null,
|
||||
scope: null,
|
||||
}
|
||||
#flashInterval = null;
|
||||
#countDownIntervalId = null;
|
||||
@ -114,6 +116,11 @@ export class HUD {
|
||||
this.#flashInterval = setTimeout(callback, fullFlashTimeMs)
|
||||
}
|
||||
|
||||
updateCrossHair(scopeLevel) {
|
||||
this.#elements.cross.style.opacity = (scopeLevel === 0 ? 1.0 : 0.0)
|
||||
this.#elements.scope.style.opacity = (scopeLevel === 0 ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
roundStart(roundTimeMs) {
|
||||
this.#startCountDown(roundTimeMs)
|
||||
}
|
||||
@ -242,7 +249,9 @@ export class HUD {
|
||||
}
|
||||
|
||||
elementHud.innerHTML = `
|
||||
<div id="scope"><div class="scope-cross"></div></div>
|
||||
<div id="flash"></div>
|
||||
<div id="hud-container">
|
||||
<div id="cross"></div>
|
||||
<div id="hit-feedback"></div>
|
||||
<div id="scoreboard" class="hidden">
|
||||
@ -307,9 +316,11 @@ export class HUD {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
|
||||
elementHud.style.setProperty('--flash-bang-color', setting.getFlashBangColor())
|
||||
elementHud.style.setProperty('--scope-size', setting.getScopeSize())
|
||||
|
||||
this.#elements.flash = elementHud.querySelector('#flash')
|
||||
this.#elements.score = elementHud.querySelector('#scoreboard')
|
||||
@ -334,9 +345,10 @@ export class HUD {
|
||||
this.#elements.aliveOpponentTeam = elementHud.querySelector('.team-opponent-alive')
|
||||
this.#elements.time = elementHud.querySelector('#time')
|
||||
this.#elements.killFeed = elementHud.querySelector('.kill-feed')
|
||||
this.#elements.cross = elementHud.querySelector('#cross')
|
||||
this.#elements.scope = elementHud.querySelector('#scope')
|
||||
|
||||
const cross = elementHud.querySelector('#cross')
|
||||
cross.innerText = setting.getCrosshairSymbol()
|
||||
this.#elements.cross.innerText = setting.getCrosshairSymbol()
|
||||
setting.addUpdateCallback('crosshairColor', (newValue) => cross.style.color = newValue)
|
||||
setting.update('crosshairColor', setting.getCrosshairColor())
|
||||
setting.addUpdateCallback('crosshairSize', (newValue) => cross.style.fontSize = newValue + 'px')
|
||||
|
@ -172,6 +172,7 @@ export class ModelRepository {
|
||||
models[ItemId.Knife] = 'knife.glb'
|
||||
models[ItemId.RifleAk] = 'ak.glb'
|
||||
models[ItemId.RifleM4A4] = 'm4.glb'
|
||||
models[ItemId.RifleAWP] = 'm4.glb' // fixme :D
|
||||
models[ItemId.PistolUsp] = 'pistol.glb' // fixme
|
||||
models[ItemId.PistolP250] = 'pistol.glb'
|
||||
models[ItemId.PistolGlock] = 'pistol.glb' // fixme
|
||||
|
@ -24,6 +24,7 @@ export class Player {
|
||||
ammo: null,
|
||||
ammoReserve: null,
|
||||
isReloading: null,
|
||||
scopeLevel: null,
|
||||
}
|
||||
#custom = {
|
||||
slotId: null,
|
||||
|
@ -8,11 +8,13 @@ export class Setting {
|
||||
volume: 20,
|
||||
radarZoom: 0.9,
|
||||
sensitivity: 1.0,
|
||||
inScopeSensitivity: 0.5,
|
||||
sprayTriggerDeltaMs: 80,
|
||||
crosshair: '✛',
|
||||
crosshairColor: '#d31b1b',
|
||||
crosshairSize: 40,
|
||||
flashBangColor: '#FFFFFF',
|
||||
scopeSize: '2px',
|
||||
preferPerformance: false,
|
||||
matchServerFps: false,
|
||||
anisotropic: 16,
|
||||
@ -127,6 +129,10 @@ export class Setting {
|
||||
return this.#setting.base.sensitivity ?? 1.0
|
||||
}
|
||||
|
||||
getInScopeSensitivity() {
|
||||
return this.#setting.base.inScopeSensitivity ?? 0.5
|
||||
}
|
||||
|
||||
getExposure() {
|
||||
return this.#setting.base.exposure ?? 0.8
|
||||
}
|
||||
@ -150,4 +156,8 @@ export class Setting {
|
||||
getFlashBangColor() {
|
||||
return this.#setting.base.flashBangColor ?? '#FFFFFF';
|
||||
}
|
||||
|
||||
getScopeSize() {
|
||||
return this.#setting.base.scopeSize ?? '2px';
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {InventorySlot, SoundType} from "./Enums.js";
|
||||
import {InventorySlot, SoundType, ItemId} from "./Enums.js";
|
||||
|
||||
export class SoundRepository {
|
||||
#alwaysInHeadTypes = [
|
||||
@ -152,6 +152,10 @@ export class SoundRepository {
|
||||
}
|
||||
|
||||
getItemAttackSound(item) {
|
||||
if (item.id === ItemId.RifleAWP) {
|
||||
return '371574__matrixxx__rifle-gun-tikka-t3-tactical-shot-04.wav'
|
||||
}
|
||||
|
||||
if (item.slot === InventorySlot.SLOT_SECONDARY) {
|
||||
return '387480__cosmicembers__dart-thud-2.wav'
|
||||
}
|
||||
|
@ -256,6 +256,11 @@ export class World {
|
||||
this.#renderer.render(this.#scene, this.#camera)
|
||||
}
|
||||
|
||||
updateCameraZoom(zoomLevel) {
|
||||
this.#camera.zoom = zoomLevel
|
||||
this.#camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
getCamera() {
|
||||
return this.#camera
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ let launchGame
|
||||
|
||||
hud.createHud(elementHud, map, setting)
|
||||
control.init(canvasParent, pointerLock, setting)
|
||||
game.setDependency(pointerLock, setting.shouldMatchServerFps())
|
||||
game.setDependency(pointerLock, setting)
|
||||
canvas.addEventListener("click", () => game.requestPointerLock())
|
||||
canvasParent.appendChild(canvas)
|
||||
stats.dom.style.position = 'inherit'
|
||||
|
BIN
www/resources/sound/210018__supakid13__sniper-scope-zoom-in.wav
Normal file
BIN
www/resources/sound/210018__supakid13__sniper-scope-zoom-in.wav
Normal file
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user