Add scope mode for AWP

This commit is contained in:
Daniel Maixner 2023-09-25 16:59:00 +02:00
parent 83ab7ed84c
commit 636bd9f19b
19 changed files with 169 additions and 7 deletions

View File

@ -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

View File

@ -326,6 +326,7 @@ final class Player
'ammo' => $ammo,
'ammoReserve' => $ammoReserve,
'isReloading' => $reloading,
'scopeLevel' => $equippedItem->getScopeLevel(),
];
}

View File

@ -0,0 +1,12 @@
<?php
namespace cs\Interface;
interface ScopeItem
{
public function scope(): void;
public function isScopedIn(): bool;
}

View File

@ -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
}

View File

@ -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()) {

View File

@ -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;

View File

@ -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);
}
}

View File

@ -105,6 +105,7 @@ class ProtocolTest extends BaseTest
'ammo' => 12,
'ammoReserve' => 120,
'isReloading' => false,
'scopeLevel' => 0,
];
$this->assertSame($playerSerializedExpected, $player->serialize());

View File

@ -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 {

View File

@ -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() {

View File

@ -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')

View File

@ -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

View File

@ -24,6 +24,7 @@ export class Player {
ammo: null,
ammoReserve: null,
isReloading: null,
scopeLevel: null,
}
#custom = {
slotId: null,

View File

@ -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';
}
}

View File

@ -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'
}

View File

@ -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
}

View File

@ -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'