mirror of
https://github.com/solcloud/Counter-Strike.git
synced 2025-04-18 22:53:40 +02:00
parent
2816c75a07
commit
287cf9ed20
@ -12,6 +12,7 @@ use cs\Core\World;
|
||||
use cs\Enum\SoundType;
|
||||
use cs\Equipment\Flashbang;
|
||||
use cs\Equipment\Grenade;
|
||||
use cs\Equipment\HighExplosive;
|
||||
use cs\HitGeometry\BallCollider;
|
||||
use cs\Interface\Attackable;
|
||||
|
||||
@ -48,7 +49,7 @@ final class ThrowEvent extends Event implements Attackable
|
||||
$this->position = $origin->clone();
|
||||
$this->lastEventPosition = $origin->clone();
|
||||
$this->ball = new BallCollider($this->world, $origin, $radius);
|
||||
$this->needsToLandOnFloor = !($this->item instanceof Flashbang);
|
||||
$this->needsToLandOnFloor = !($this->item instanceof Flashbang || $this->item instanceof HighExplosive);
|
||||
$this->timeIncrement = 1 / Util::millisecondsToFrames(150); // fixme some good value or velocity or gravity :)
|
||||
$this->tickMax = $this->getTickId() + Util::millisecondsToFrames($maxTimeMs);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Test\Unit;
|
||||
|
||||
use cs\Core\Box;
|
||||
use cs\Core\Floor;
|
||||
use cs\Core\GameFactory;
|
||||
use cs\Core\Plane;
|
||||
@ -16,6 +17,43 @@ use Test\BaseTest;
|
||||
class BallColliderTest extends BaseTest
|
||||
{
|
||||
|
||||
public function testResolution1(): void
|
||||
{
|
||||
/////
|
||||
$radius = 2;
|
||||
$angleHorizontal = 0.0;
|
||||
$angleVertical = -90.0;
|
||||
$start = new Point(10, 10, 0);
|
||||
$resolutionPoint = new Point(10, 4, 0);
|
||||
$resolutionAngleHorizontal = $angleHorizontal;
|
||||
$resolutionAngleVertical = 90.0;
|
||||
/////
|
||||
|
||||
$ball = $this->createBall($start, $radius, $world);
|
||||
$world->addBox(new Box(new Point(9), 1, 1, 1));
|
||||
$world->addBox(new Box(new Point(10), 1, 1, 1));
|
||||
$world->addBox(new Box(new Point(11), 1, 1, 1));
|
||||
|
||||
$this->runCollision($ball, $start, $angleHorizontal, $angleVertical);
|
||||
$this->assertPositionSame($resolutionPoint, $ball->getLastValidPosition());
|
||||
$this->assertSame($resolutionAngleHorizontal, $ball->getResolutionAngleHorizontal());
|
||||
$this->assertSame($resolutionAngleVertical, $ball->getResolutionAngleVertical());
|
||||
}
|
||||
|
||||
protected function runCollision(BallCollider $ball, Point $start, float $angleHorizontal, float $angleVertical): void
|
||||
{
|
||||
$candidate = $start->clone();
|
||||
for ($distance = 1; $distance <= 128; $distance++) {
|
||||
$candidate->setFrom($start);
|
||||
$candidate->addFromArray(Util::movementXYZ($angleHorizontal, $angleVertical, $distance));
|
||||
if ($ball->hasCollision($candidate, $angleHorizontal, $angleVertical)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->fail("No '{$start}' collision detected");
|
||||
}
|
||||
|
||||
public function testSingleWallBounce(): void
|
||||
{
|
||||
$this->_testSingleWallBounce(new Point(5, 5, 11), 4, 0, 90, new Floor(new Point(5, 16, 11)), 0, -90);
|
||||
@ -82,6 +120,12 @@ class BallColliderTest extends BaseTest
|
||||
$this->fail('No collision detected');
|
||||
}
|
||||
|
||||
private function createBall(Point $start, int $radius, ?World &$world): BallCollider
|
||||
{
|
||||
$world = $world ?? $this->createWorld();
|
||||
return new BallCollider($world, $start, $radius);
|
||||
}
|
||||
|
||||
private function createWorld(): World
|
||||
{
|
||||
$game = GameFactory::createDebug();
|
||||
|
@ -8,6 +8,7 @@
|
||||
--color-me-hsl: 84, 64%, 64%;
|
||||
--color-opponent: #de6868;
|
||||
--color-opponent-hsl: 0, 64%, 64%;
|
||||
--flash-bang-color: #FFFFFF;
|
||||
}
|
||||
|
||||
#hud .color-me {
|
||||
@ -44,6 +45,14 @@
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#hud #flash {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--flash-bang-color);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#hud #hit-feedback {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
@ -174,12 +174,64 @@ export class Game {
|
||||
grenade.position.set(data.position.x, data.position.y, -data.position.z)
|
||||
}
|
||||
if (data.type === SoundType.GRENADE_LAND) {
|
||||
setTimeout(() => this.removeGrenade(data.extra.id), 1000) // todo responsive volumetric smokes, flashes, fire etc.
|
||||
this.#grenadeLand(data.extra.id, data.item, data.player, data.position)
|
||||
}
|
||||
|
||||
this.#soundRepository.play(data, spectatorId, this.#tick)
|
||||
}
|
||||
|
||||
#grenadeLand(throwableId, item, playerId, position) {
|
||||
if (item.slot === InventorySlot.SLOT_GRENADE_DECOY) {
|
||||
const player = this.players[playerId]
|
||||
const soundItem = player.data.slots[InventorySlot.SLOT_PRIMARY] ? player.data.slots[InventorySlot.SLOT_PRIMARY] : (player.data.slots[InventorySlot.SLOT_SECONDARY] ? player.data.slots[InventorySlot.SLOT_SECONDARY] : player.data.slots[InventorySlot.SLOT_KNIFE])
|
||||
const soundName = this.#soundRepository.getItemAttackSound(soundItem)
|
||||
|
||||
const game = this
|
||||
const world = this.#world
|
||||
let endTime = Date.now() + 15 * 1E6
|
||||
const callback = function () {
|
||||
world.playSound(soundName, position, false)
|
||||
if (Date.now() > endTime) {
|
||||
game.removeGrenade(throwableId)
|
||||
return
|
||||
}
|
||||
setTimeout(callback, Math.random() * 1000)
|
||||
}
|
||||
setTimeout(callback, 100)
|
||||
return
|
||||
}
|
||||
if (item.slot === InventorySlot.SLOT_GRENADE_FLASH) {
|
||||
const grenade = this.#throwables[throwableId]
|
||||
const sight = this.playerSpectate.get3DObject().getObjectByName('sight')
|
||||
const sightPosition = sight.getWorldPosition(new THREE.Vector3())
|
||||
const direction = grenade.getWorldPosition(new THREE.Vector3()).sub(sightPosition).normalize()
|
||||
if (this.#world.getCamera().getWorldDirection(new THREE.Vector3()).dot(direction) <= 0) { // flash behind spectator
|
||||
this.removeGrenade(throwableId)
|
||||
return;
|
||||
}
|
||||
|
||||
const ray = new THREE.Raycaster(sightPosition, direction)
|
||||
const intersects = ray.intersectObjects([grenade, ...this.#world.getMapObjects()]);
|
||||
if (intersects.length >= 1 && intersects[0].object === grenade) {
|
||||
this.#hud.showFlashBangScreen()
|
||||
}
|
||||
this.removeGrenade(throwableId)
|
||||
return;
|
||||
}
|
||||
if (item.slot === InventorySlot.SLOT_GRENADE_SMOKE) {
|
||||
const grenade = this.#throwables[throwableId]
|
||||
const smoke = new THREE.Mesh(new THREE.DodecahedronGeometry(300, 1), new THREE.MeshStandardMaterial({color: 0xdadada}))
|
||||
smoke.material.side = THREE.DoubleSide
|
||||
smoke.position.y = 150
|
||||
grenade.add(smoke)
|
||||
setTimeout(() => this.removeGrenade(throwableId), 18000)
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn("No handler for grenade: ", item)
|
||||
setTimeout(() => this.removeGrenade(throwableId), 1000) // todo responsive volumetric smokes, flashes, fire etc.
|
||||
}
|
||||
|
||||
spawnGrenade(item, id) {
|
||||
this.#throwables[id] = this.#world.spawnGrenade(item)
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ export class HUD {
|
||||
showGameMenu: false,
|
||||
}
|
||||
#elements = {
|
||||
flash: null,
|
||||
score: null,
|
||||
scoreDetail: null,
|
||||
buyMenu: null,
|
||||
@ -45,6 +46,7 @@ export class HUD {
|
||||
time: null,
|
||||
killFeed: null,
|
||||
}
|
||||
#flashInterval = null;
|
||||
#dropAnimationInterval = null;
|
||||
#countDownIntervalId = null;
|
||||
#scoreBoardData = null;
|
||||
@ -98,6 +100,23 @@ export class HUD {
|
||||
this.#killFeed.showKill(playerCulprit, playerDead, wasHeadshot, playerMe, killedItemId)
|
||||
}
|
||||
|
||||
showFlashBangScreen(fullFlashTimeMs = 1000, resetTimeMs = 3000) {
|
||||
clearTimeout(this.#flashInterval)
|
||||
const element = this.#elements.flash
|
||||
element.style.opacity = 1.0
|
||||
const timePortion = resetTimeMs / 100
|
||||
|
||||
const callback = function () {
|
||||
element.style.opacity -= 0.01
|
||||
if (element.style.opacity < .01) {
|
||||
element.style.opacity = 0
|
||||
} else {
|
||||
setTimeout(callback, timePortion)
|
||||
}
|
||||
}
|
||||
this.#flashInterval = setTimeout(callback, fullFlashTimeMs)
|
||||
}
|
||||
|
||||
roundStart(roundTimeMs) {
|
||||
this.#startCountDown(roundTimeMs)
|
||||
}
|
||||
@ -229,6 +248,7 @@ export class HUD {
|
||||
}
|
||||
|
||||
elementHud.innerHTML = `
|
||||
<div id="flash"></div>
|
||||
<div id="cross"></div>
|
||||
<div id="hit-feedback"></div>
|
||||
<div id="equipped-item">
|
||||
@ -289,7 +309,7 @@ export class HUD {
|
||||
<p class="hidden" data-slot="${Enum.InventorySlot.SLOT_BOMB}">Bomb</p>
|
||||
<p class="hidden" data-slot="${Enum.InventorySlot.SLOT_GRENADE_SMOKE}">Smoke</p>
|
||||
<p class="hidden" data-slot="${Enum.InventorySlot.SLOT_GRENADE_FLASH}">Flash</p>
|
||||
<p class="hidden" data-slot="${Enum.InventorySlot.SLOT_GRENADE_HE}">HighExplosive</p>
|
||||
<p class="hidden" data-slot="${Enum.InventorySlot.SLOT_GRENADE_HE}">Frag</p>
|
||||
<p class="hidden" data-slot="${Enum.InventorySlot.SLOT_GRENADE_MOLOTOV}">Molotov</p>
|
||||
<p class="hidden" data-slot="${Enum.InventorySlot.SLOT_GRENADE_DECOY}">Decoy</p>
|
||||
</div>
|
||||
@ -300,6 +320,9 @@ export class HUD {
|
||||
</section>
|
||||
`;
|
||||
|
||||
elementHud.style.setProperty('--flash-bang-color', setting.getFlashBangColor())
|
||||
|
||||
this.#elements.flash = elementHud.querySelector('#flash')
|
||||
this.#elements.score = elementHud.querySelector('#scoreboard')
|
||||
this.#elements.buyMenu = elementHud.querySelector('#buy-menu')
|
||||
this.#elements.gameMenu = elementHud.querySelector('#game-menu')
|
||||
|
@ -14,6 +14,7 @@ export class ModelRepository {
|
||||
#textures = {
|
||||
cap: {}
|
||||
}
|
||||
#mapObjects = [];
|
||||
|
||||
constructor() {
|
||||
this.#gltfLoader = new THREE.GLTFLoader()
|
||||
@ -30,12 +31,19 @@ export class ModelRepository {
|
||||
return this.#textureLoader.loadAsync(url)
|
||||
}
|
||||
|
||||
getMapObjects() {
|
||||
return this.#mapObjects
|
||||
}
|
||||
|
||||
loadMap(mapName) {
|
||||
const self = this
|
||||
self.#mapObjects = []
|
||||
return this.#loadModel(`./resources/map/${mapName}.glb`).then((model) => {
|
||||
model.scene.traverse(function (object) {
|
||||
if (object.isMesh) {
|
||||
if (object.name !== 'world') {
|
||||
object.castShadow = true
|
||||
self.#mapObjects.push(object)
|
||||
}
|
||||
if (object.name === 'floor') {
|
||||
object.material.envMapIntensity = .08
|
||||
@ -51,7 +59,6 @@ export class ModelRepository {
|
||||
const sun = new THREE.DirectionalLight(0xffeac2, .9)
|
||||
sun.position.set(4000, 4999, -4000)
|
||||
sun.castShadow = true
|
||||
//sun.shadow.bias = .0001
|
||||
sun.shadow.mapSize.width = 4096
|
||||
sun.shadow.mapSize.height = 4096
|
||||
sun.shadow.camera.far = 10000
|
||||
@ -112,7 +119,9 @@ export class ModelRepository {
|
||||
}
|
||||
|
||||
loadAll() {
|
||||
const self = this
|
||||
const promises = []
|
||||
|
||||
promises.push(this.#loadModel('./resources/model/player.glb').then((model) => {
|
||||
model.scene.traverse(function (object) {
|
||||
object.frustumCulled = false // fixme find out how to recalculate bounding boxes or bake animation
|
||||
@ -135,18 +144,39 @@ export class ModelRepository {
|
||||
this.#models.player = model.scene.getObjectByName('player')
|
||||
this.#models.playerAnimation = model.animations
|
||||
}))
|
||||
promises.push(this.#loadModel('./resources/model/bomb.glb').then((model) => {
|
||||
model.scene.children.forEach((root) => root.visible = false)
|
||||
const item = model.scene.getObjectByName('item')
|
||||
item.traverse(function (object) {
|
||||
if (object.isMesh) {
|
||||
object.castShadow = true
|
||||
}
|
||||
})
|
||||
item.visible = true
|
||||
|
||||
this.#models[ItemId.Bomb] = model.scene
|
||||
}))
|
||||
const models = {}
|
||||
models[ItemId.Bomb] = 'bomb.glb'
|
||||
models[ItemId.Knife] = 'knife.glb'
|
||||
models[ItemId.RifleAk] = 'ak.glb'
|
||||
models[ItemId.RifleM4A4] = 'm4.glb'
|
||||
models[ItemId.PistolUsp] = 'pistol.glb' // fixme
|
||||
models[ItemId.PistolP250] = 'pistol.glb'
|
||||
models[ItemId.PistolGlock] = 'pistol.glb' // fixme
|
||||
if (false) { // fixme
|
||||
models[ItemId.HighExplosive] = 'highexplosive.glb'
|
||||
models[ItemId.Flashbang] = 'flashbang.glb'
|
||||
models[ItemId.Smoke] = 'smoke.glb'
|
||||
models[ItemId.Decoy] = 'decoy.glb'
|
||||
models[ItemId.Incendiary] = 'incendiary.glb'
|
||||
models[ItemId.Molotov] = 'molotov.glb'
|
||||
}
|
||||
|
||||
Object.keys(models).forEach(function (itemId) {
|
||||
const fileName = models[itemId]
|
||||
promises.push(self.#loadModel(`./resources/model/${fileName}`).then((model) => {
|
||||
model.scene.children.forEach((root) => root.visible = false)
|
||||
const item = model.scene.getObjectByName('item')
|
||||
item.traverse(function (object) {
|
||||
if (object.isMesh) {
|
||||
object.castShadow = true
|
||||
}
|
||||
})
|
||||
item.visible = true
|
||||
|
||||
self.#models[itemId] = model.scene
|
||||
}))
|
||||
})
|
||||
promises.push(this.#loadModel('./resources/model/kit.glb').then((model) => {
|
||||
model.scene.traverse(function (object) {
|
||||
if (object.isMesh) {
|
||||
@ -156,60 +186,7 @@ export class ModelRepository {
|
||||
|
||||
this.#models[ItemId.DefuseKit] = model.scene
|
||||
}))
|
||||
promises.push(this.#loadModel('./resources/model/knife.glb').then((model) => {
|
||||
model.scene.children.forEach((root) => root.visible = false)
|
||||
const item = model.scene.getObjectByName('item')
|
||||
item.traverse(function (object) {
|
||||
if (object.isMesh) {
|
||||
object.castShadow = true
|
||||
}
|
||||
})
|
||||
item.visible = true
|
||||
|
||||
this.#models[ItemId.Knife] = model.scene
|
||||
}))
|
||||
promises.push(this.#loadModel('./resources/model/pistol.glb').then((model) => {
|
||||
model.scene.children.forEach((root) => root.visible = false)
|
||||
const item = model.scene.getObjectByName('item')
|
||||
item.traverse(function (object) {
|
||||
if (object.isMesh) {
|
||||
object.castShadow = true
|
||||
}
|
||||
})
|
||||
item.visible = true
|
||||
|
||||
this.#models[ItemId.PistolUsp] = model.scene
|
||||
this.#models[ItemId.PistolP250] = model.scene // fixme
|
||||
this.#models[ItemId.PistolGlock] = model.scene // fixme
|
||||
}))
|
||||
promises.push(this.#loadModel('./resources/model/ak.glb').then((model) => {
|
||||
model.scene.children.forEach((root) => root.visible = false)
|
||||
const item = model.scene.getObjectByName('item')
|
||||
item.traverse(function (object) {
|
||||
if (object.isMesh) {
|
||||
object.castShadow = true
|
||||
}
|
||||
})
|
||||
item.visible = true
|
||||
|
||||
this.#models[ItemId.RifleAk] = model.scene
|
||||
}))
|
||||
promises.push(this.#loadModel('./resources/model/m4.glb').then((model) => {
|
||||
model.scene.children.forEach((root) => root.visible = false)
|
||||
const item = model.scene.getObjectByName('item')
|
||||
item.traverse(function (object) {
|
||||
if (object.isMesh) {
|
||||
object.castShadow = true
|
||||
}
|
||||
})
|
||||
item.visible = true
|
||||
|
||||
// fixme inside model
|
||||
const povSpark = model.scene.getObjectByName('pov-spark')
|
||||
povSpark.position.z = 0
|
||||
|
||||
this.#models[ItemId.RifleM4A4] = model.scene
|
||||
}))
|
||||
promises.push(this.#loadTexture('./resources/img/player/outfit_0.png').then((texture) => {
|
||||
texture.flipY = false
|
||||
texture.encoding = THREE.sRGBEncoding
|
||||
@ -220,26 +197,13 @@ export class ModelRepository {
|
||||
texture.encoding = THREE.sRGBEncoding
|
||||
this.#textures.opponent = texture
|
||||
}))
|
||||
promises.push(this.#loadTexture('./resources/img/player/cap_1.png').then((texture) => {
|
||||
texture.flipY = false
|
||||
this.#textures.cap[1] = texture
|
||||
}))
|
||||
promises.push(this.#loadTexture('./resources/img/player/cap_2.png').then((texture) => {
|
||||
texture.flipY = false
|
||||
this.#textures.cap[2] = texture
|
||||
}))
|
||||
promises.push(this.#loadTexture('./resources/img/player/cap_3.png').then((texture) => {
|
||||
texture.flipY = false
|
||||
this.#textures.cap[3] = texture
|
||||
}))
|
||||
promises.push(this.#loadTexture('./resources/img/player/cap_4.png').then((texture) => {
|
||||
texture.flipY = false
|
||||
this.#textures.cap[4] = texture
|
||||
}))
|
||||
promises.push(this.#loadTexture('./resources/img/player/cap_5.png').then((texture) => {
|
||||
texture.flipY = false
|
||||
this.#textures.cap[5] = texture
|
||||
}))
|
||||
for (let number = 1; number <= 5; number++) {
|
||||
promises.push(self.#loadTexture(`./resources/img/player/cap_${number}.png`).then((texture) => {
|
||||
texture.flipY = false
|
||||
self.#textures.cap[number] = texture
|
||||
}))
|
||||
}
|
||||
|
||||
promises.push(this.#loadTexture('./resources/img/sphere_glow.png').then((texture) => {
|
||||
const material = new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
@ -260,6 +224,10 @@ export class ModelRepository {
|
||||
const materialOpponent = outfit.material.clone()
|
||||
materialOpponent.map = this.#textures.opponent
|
||||
|
||||
// fixme inside model
|
||||
const povSpark = self.#models[ItemId.RifleM4A4].getObjectByName('pov-spark')
|
||||
povSpark.position.z = 0
|
||||
|
||||
this.#materials.outfitTeam = materialTeam
|
||||
this.#materials.outfitOpponent = materialOpponent
|
||||
})
|
||||
|
@ -12,6 +12,7 @@ export class Setting {
|
||||
crosshair: '✛',
|
||||
crosshairColor: '#d31b1b',
|
||||
crosshairSize: 40,
|
||||
flashBangColor: '#FFFFFF',
|
||||
preferPerformance: false,
|
||||
matchServerFps: false,
|
||||
anisotropic: 16,
|
||||
@ -131,4 +132,7 @@ export class Setting {
|
||||
return this.#setting.base.crosshairSize ?? 40
|
||||
}
|
||||
|
||||
getFlashBangColor() {
|
||||
return this.#setting.base.flashBangColor ?? '#FFFFFF';
|
||||
}
|
||||
}
|
||||
|
@ -48,19 +48,7 @@ export class SoundRepository {
|
||||
}
|
||||
|
||||
if (type === SoundType.ITEM_ATTACK) {
|
||||
if (item.slot === InventorySlot.SLOT_SECONDARY) {
|
||||
return '387480__cosmicembers__dart-thud-2.wav'
|
||||
}
|
||||
if (item.slot === InventorySlot.SLOT_PRIMARY) {
|
||||
return '513421__pomeroyjoshua__anu-clap-09.wav'
|
||||
}
|
||||
if (item.slot === InventorySlot.SLOT_KNIFE) {
|
||||
return '240788__f4ngy__knife-hitting-wood.wav'
|
||||
}
|
||||
if (this.#grenadesSlots.includes(item.slot)) {
|
||||
return '163458__lemudcrab__grenade-launcher.wav'
|
||||
}
|
||||
return '558117__abdrtar__move.mp3'
|
||||
return this.getItemAttackSound(item)
|
||||
}
|
||||
if (type === SoundType.ITEM_ATTACK2) {
|
||||
if (item.slot === InventorySlot.SLOT_KNIFE) {
|
||||
@ -160,4 +148,20 @@ export class SoundRepository {
|
||||
return null
|
||||
}
|
||||
|
||||
getItemAttackSound(item) {
|
||||
if (item.slot === InventorySlot.SLOT_SECONDARY) {
|
||||
return '387480__cosmicembers__dart-thud-2.wav'
|
||||
}
|
||||
if (item.slot === InventorySlot.SLOT_PRIMARY) {
|
||||
return '513421__pomeroyjoshua__anu-clap-09.wav'
|
||||
}
|
||||
if (item.slot === InventorySlot.SLOT_KNIFE) {
|
||||
return '240788__f4ngy__knife-hitting-wood.wav'
|
||||
}
|
||||
if (this.#grenadesSlots.includes(item.slot)) {
|
||||
return '163458__lemudcrab__grenade-launcher.wav'
|
||||
}
|
||||
return '558117__abdrtar__move.mp3'
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -134,6 +134,10 @@ export class World {
|
||||
return model
|
||||
}
|
||||
|
||||
getMapObjects() {
|
||||
return this.#modelRepository.getMapObjects()
|
||||
}
|
||||
|
||||
itemAttack(player, item, isSpectator) {
|
||||
const sparkFeedbackSlots = [Enum.InventorySlot.SLOT_PRIMARY, Enum.InventorySlot.SLOT_SECONDARY]
|
||||
if (sparkFeedbackSlots.includes(item.slot)) {
|
||||
|
@ -57,7 +57,7 @@ export class BuyMenu {
|
||||
: ``
|
||||
}
|
||||
${playerData.slots[Enum.InventorySlot.SLOT_GRENADE_HE] === undefined
|
||||
? `<p${money < 300 ? ' class="disabled"' : ''}><a data-buy-menu-item-id="${Enum.BuyMenuItem.GRENADE_HE}" class="hud-action action-buy">HE for ${this.#formatPrice(300)}</a></p>`
|
||||
? `<p${money < 300 ? ' class="disabled"' : ''}><a data-buy-menu-item-id="${Enum.BuyMenuItem.GRENADE_HE}" class="hud-action action-buy">Frag for ${this.#formatPrice(300)}</a></p>`
|
||||
: ``
|
||||
}
|
||||
${playerData.slots[Enum.InventorySlot.SLOT_GRENADE_MOLOTOV] === undefined
|
||||
|
Loading…
x
Reference in New Issue
Block a user