Загрузка данных
/**
* Спанк / свист / алко / термокружка — клиент + синхрон по переменным.
* Asti / медовуха: beer-анима + p_cs_bottle_01.
* Термокружка: shr_thermos + coffee-анима.
*/
const FLAGS_UPPER_BODY = 49;
const SPANK_MS = 2450;
const WHISTLE_MS = 2260;
const ALCO_MS = 5250;
const THERMO_MS = 5250;
const BONG_MS = 2000;
const SPANK_DICT = 'mp_suicide';
const SPANK_NAME = 'pill_fp';
const BONG_DICT = 'gta5rp@alterned';
const BONG_NAME = 'smoking';
const WHISTLE_DICT = 'rcmnigel1c';
const WHISTLE_NAME = 'hailing_whistle_waive_a';
// Второй свист (твой вариант)
const WHISTLE2_MS = 3500;
const WHISTLE2_DICT = 'rcmnigel1c';
const WHISTLE2_NAME = 'hailing_whistle_waive_a';
let selectedWhistle = 1; // 1 = обычный, 2 = твой новый
try {
if (mp.storage.data.selectedWhistle) {
selectedWhistle = Number(mp.storage.data.selectedWhistle) || 1;
}
} catch (e) {}
const ALCO_DICT = 'amb@world_human_drinking@beer@male@idle_a';
const ALCO_NAME = 'idle_b';
const ALCO_PROP_MODEL = 'p_cs_bottle_01';
const ALCO_PROP_HASH = mp.game.joaat(ALCO_PROP_MODEL);
const ALCO_PROP_OFFSET = { x: 0.11, y: -0.02, z: -0.04 };
const ALCO_PROP_ROT = { x: -82, y: 8, z: 4 };
const CIGARETTE_PROP_HASH = 175300549;
const CIGARETTE_PROP_OFFSET = { x: 0.11, y: 0.0, z: -0.02 };
const CIGARETTE_PROP_ROT = { x: -85, y: 5, z: 5 };
const THERMO_DICT = 'amb@world_human_drinking@coffee@male@idle_a';
const THERMO_NAME = 'idle_a';
const THERMO_HAND_BONE = 57005;
const THERMO_PROP_MODEL = 'shr_thermos';
const THERMO_PROP_HASH = mp.game.joaat(THERMO_PROP_MODEL);
const THERMO_PROP_SCALE = 1.16;
const THERMO_ATTACH = {
bone: THERMO_HAND_BONE,
offset: { x: 0.110, y: -0.100, z: -0.045 },
rot: { x: -80, y: 19, z: 6 },
};
let THERMO_PROP_SCALE_LIVE = THERMO_PROP_SCALE;
let thermoPropModelName = THERMO_PROP_MODEL;
let thermoPropHash = THERMO_PROP_HASH;
function getThermoUseProfile() {
return {
dict: THERMO_DICT,
name: THERMO_NAME,
attach: THERMO_ATTACH,
};
}
function getThermoAttach() {
return THERMO_ATTACH;
}
const SYNC_CONFIG = [
{ varName: 'ANIM_SPANK', dict: SPANK_DICT, name: SPANK_NAME, dur: SPANK_MS, speedIn: 8.0, speedOut: 1.0 },
{ varName: 'ANIM_WHISTLE', dict: WHISTLE_DICT, name: WHISTLE_NAME, dur: WHISTLE_MS, speedIn: 8.0, speedOut: -8.0 },
{ varName: 'ANIM_ALCOHOL', dict: ALCO_DICT, name: ALCO_NAME, dur: ALCO_MS, speedIn: 8.0, speedOut: 1.0, prop: 'alco' },
{ varName: 'ANIM_THERMOS', dict: THERMO_DICT, name: THERMO_NAME, dur: THERMO_MS, speedIn: 8.0, speedOut: 1.0, prop: 'thermo' },
{ varName: 'ANIM_BONG', dict: BONG_DICT, name: BONG_NAME, dur: BONG_MS, speedIn: 8.0, speedOut: 1.0 },
{ varName: 'ANIM_CIGARETTE', dict: BONG_DICT, name: BONG_NAME, dur: BONG_MS, speedIn: 8.0, speedOut: 1.0, prop: 'cigarette' },
];
let itemUseAnimInProgress = false;
function finishConsumableUseAnim(player, dict, name) {
if (!player || !mp.players.exists(player)) return;
if (player.vehicle) {
try {
if (dict && name && typeof player.stopAnimTask === 'function') {
player.stopAnimTask(dict, name, 3.0);
}
} catch (_e) {}
} else {
try { player.clearTasks(); } catch (_e2) {}
}
}
function beginConsumableUseAnim() {
if (itemUseAnimInProgress) return false;
itemUseAnimInProgress = true;
return true;
}
const handPropByRemoteId = new Map();
function requestAnimDict(dict) {
return new Promise((resolve) => {
if (mp.game.streaming.hasAnimDictLoaded(dict)) {
resolve(true);
return;
}
mp.game.streaming.requestAnimDict(dict);
let n = 0;
const id = setInterval(() => {
if (mp.game.streaming.hasAnimDictLoaded(dict) || ++n > 400) {
clearInterval(id);
resolve(mp.game.streaming.hasAnimDictLoaded(dict));
}
}, 25);
});
}
function loadModel(hash, forceRequest) {
return new Promise((resolve) => {
if (!hash) {
resolve(false);
return;
}
try {
if (!forceRequest && mp.game.streaming.isModelInCdimage && !mp.game.streaming.isModelInCdimage(hash)) {
resolve(false);
return;
}
} catch (_e) {}
mp.game.streaming.requestModel(hash);
let n = 0;
const id = setInterval(() => {
if (mp.game.streaming.hasModelLoaded(hash) || ++n > 400) {
clearInterval(id);
resolve(mp.game.streaming.hasModelLoaded(hash));
}
}, 25);
});
}
function isThermoPropModelHash(hash) {
if (!hash) return false;
return hash === THERMO_PROP_HASH || (!!thermoPropHash && hash === thermoPropHash);
}
function resolveThermoPropHash(forceRescan) {
if (forceRescan) {
thermoPropHash = 0;
thermoPropModelName = '';
}
if (!forceRescan && thermoPropHash && thermoPropModelName) {
return Promise.resolve(thermoPropHash);
}
return loadModel(THERMO_PROP_HASH, true).then((ok) => {
if (!ok) {
thermoPropHash = 0;
thermoPropModelName = '';
return 0;
}
thermoPropModelName = THERMO_PROP_MODEL;
thermoPropHash = THERMO_PROP_HASH;
return THERMO_PROP_HASH;
});
}
function scaleThermosProp(obj, scale) {
if (!obj || !obj.handle || !scale || scale === 1) return;
try {
const right = new mp.Vector3(0, 0, 0);
const forward = new mp.Vector3(0, 0, 0);
const up = new mp.Vector3(0, 0, 0);
const pos = new mp.Vector3(0, 0, 0);
obj.getMatrix(right, forward, up, pos);
forward.x *= scale; forward.y *= scale; forward.z *= scale;
right.x *= scale; right.y *= scale; right.z *= scale;
up.x *= scale; up.y *= scale; up.z *= scale;
mp.game.invoke(
'0x694E00132F2823ED',
obj.handle,
forward.x, forward.y, forward.z,
right.x, right.y, right.z,
up.x, up.y, up.z,
pos.x, pos.y, pos.z
);
} catch (_e) {}
}
function getHandBoneIndex(ped, boneId) {
const id = boneId == null ? 57005 : Number(boneId);
if (id === 28422) {
try {
if (typeof ped.getBoneIndexByName === 'function') {
const ph = ped.getBoneIndexByName('PH_R_Hand');
if (ph != null && ph >= 0) return ph;
}
} catch (_e) {}
}
if (id === 57005) {
try {
if (typeof ped.getBoneIndexByName === 'function') {
const i = ped.getBoneIndexByName('SKEL_R_Hand');
if (i != null && i >= 0) return i;
}
} catch (_e) {}
}
try {
if (ped && ped.handle && mp.game.ped && typeof mp.game.ped.getBoneIndex === 'function') {
const nativeBone = mp.game.ped.getBoneIndex(ped.handle, id);
if (nativeBone != null && nativeBone >= 0) return nativeBone;
}
} catch (_e) {}
try {
const j = ped.getBoneIndex(id);
if (j != null && j >= 0) return j;
} catch (_e2) {}
return id;
}
function pedStillValid(ped) {
if (!ped) return false;
try {
if (ped.type === 'player' && !mp.players.exists(ped)) return false;
return !!ped.handle;
} catch (_e) {
return false;
}
}
function clearHandProp(remoteId) {
const rid = Number(remoteId);
const entry = handPropByRemoteId.get(rid);
if (!entry) return;
if (entry.attachIntervalId) {
try { clearInterval(entry.attachIntervalId); } catch (_e) {}
}
if (entry.timerId) {
try { clearTimeout(entry.timerId); } catch (_e) {}
}
try {
const o = entry.obj;
if (o && typeof mp.objects.exists === 'function' && mp.objects.exists(o)) o.destroy();
else if (o && o.destroy) o.destroy();
} catch (_e) {}
handPropByRemoteId.delete(rid);
}
function attachPropToPedHand(obj, ped, offset, rot, boneId, useSoftPin, applyThermoScale) {
if (!obj || !pedStillValid(ped)) return;
const bone = getHandBoneIndex(ped, boneId == null ? 57005 : boneId);
try {
obj.__attMgrData = {
targetEntity: ped.handle,
bone,
offset: new mp.Vector3(offset.x, offset.y, offset.z),
rotation: new mp.Vector3(rot.x, rot.y, rot.z),
};
} catch (_e) {}
try {
obj.attachTo(
ped.handle,
bone,
offset.x,
offset.y,
offset.z,
rot.x,
rot.y,
rot.z,
false,
!!useSoftPin,
false,
false,
2,
true
);
if (applyThermoScale) scaleThermosProp(obj, THERMO_PROP_SCALE_LIVE);
} catch (_e) {}
}
function spawnHandPropAttached(ped, durationMs, modelHash, offset, rot, boneId, attachDelays) {
if (!pedStillValid(ped)) return;
const rid = ped.remoteId;
clearHandProp(rid);
const isThermoProp = isThermoPropModelHash(modelHash);
loadModel(modelHash, isThermoProp).then((modelOk) => {
if (!modelOk || !pedStillValid(ped)) {
if (ped === mp.players.local && isThermoProp) {
mp.events.call('notify:client', 2, 'top-mid', 'Модель термокружки не загрузилась.', 3000);
}
return;
}
let obj = null;
try {
const pos = ped.position;
const dim = typeof ped.dimension === 'number' ? ped.dimension : 0;
obj = mp.objects.new(
modelHash,
new mp.Vector3(pos.x, pos.y, pos.z + 0.2),
{ dimension: dim }
);
} catch (_e) {
return;
}
if (!obj) return;
try {
obj.notifyStreaming = true;
} catch (_e) {}
const attachOnce = () => attachPropToPedHand(obj, ped, offset, rot, boneId, false, isThermoProp);
const delays = isThermoProp
? [0, 80, 250]
: (Array.isArray(attachDelays) && attachDelays.length ? attachDelays : [0, 80, 250, 600, 1200]);
delays.forEach((ms) => setTimeout(attachOnce, ms));
const timerId = setTimeout(() => clearHandProp(rid), durationMs);
handPropByRemoteId.set(rid, { obj, timerId, attachIntervalId: null });
});
}
function playLocalAnim(dict, name, durationMs, speedIn, speedOut) {
const si = speedIn !== undefined ? speedIn : 8.0;
const so = speedOut !== undefined ? speedOut : -8.0;
requestAnimDict(dict).then((ok) => {
if (!ok) return;
mp.players.local.taskPlayAnim(dict, name, si, so, durationMs, FLAGS_UPPER_BODY, 0, false, false, false);
});
}
function playAlcoUseAnim(propHash, propOffset, propRot, dict, name, durationMs, boneId, propDelayMs) {
const player = mp.players.local;
if (!player || !beginConsumableUseAnim()) return;
requestAnimDict(dict).then((ok) => {
if (!ok) {
itemUseAnimInProgress = false;
return;
}
player.taskPlayAnim(dict, name, 8.0, 1.0, durationMs, FLAGS_UPPER_BODY, 0, false, false, false);
const spawnProp = () => {
spawnHandPropAttached(
player,
durationMs,
propHash,
propOffset,
propRot,
boneId,
[0, 100, 300, 700, 1400]
);
};
const delay = Number(propDelayMs) || 0;
if (delay > 0) setTimeout(spawnProp, delay);
else spawnProp();
setTimeout(() => {
finishConsumableUseAnim(player, dict, name);
itemUseAnimInProgress = false;
}, durationMs);
});
}
mp.events.add('playSpankConsumableAnim', () => {
const player = mp.players.local;
if (!player || !beginConsumableUseAnim()) return;
requestAnimDict(SPANK_DICT).then((ok) => {
if (!ok) {
itemUseAnimInProgress = false;
return;
}
player.taskPlayAnim(SPANK_DICT, SPANK_NAME, 8.0, 1.0, SPANK_MS, FLAGS_UPPER_BODY, 0, false, false, false);
setTimeout(() => {
finishConsumableUseAnim(player, SPANK_DICT, SPANK_NAME);
itemUseAnimInProgress = false;
}, SPANK_MS);
});
});
mp.events.add('cancelSpankConsumableAnim', () => {
itemUseAnimInProgress = false;
const player = mp.players.local;
if (!player) return;
finishConsumableUseAnim(player, SPANK_DICT, SPANK_NAME);
});
let whistleLoading = false;
let nextWhistleAt = 0;
mp.events.add('playWhistleLocalAnim', async () => {
const player = mp.players.local;
if (!player || !player.handle) return;
if (whistleLoading || Date.now() < nextWhistleAt) return;
whistleLoading = true;
try {
if (selectedWhistle === 2) {
// === Твой новый свист ===
mp.game.streaming.requestAnimDict(WHISTLE2_DICT);
const deadline = Date.now() + 5000;
while (!mp.game.streaming.hasAnimDictLoaded(WHISTLE2_DICT)) {
if (Date.now() >= deadline) throw new Error('Animation dictionary loading timed out');
await mp.game.waitAsync(0);
}
if (!player.handle) return;
const blend = { f_17: 0.125, f_18: 0.125 };
player.taskScriptedAnimation({
dict: WHISTLE2_DICT,
anim: WHISTLE2_NAME,
boneMaskHash: mp.game.joaat('BONEMASK_HEAD_NECK_AND_L_ARM') >> 0,
animationFlags: 48,
f_17: 2.7,
f_18: 2.7,
}, blend, blend, 0.5, 0.5);
nextWhistleAt = Date.now() + WHISTLE2_MS;
} else {
// === Обычный свист ===
playLocalAnim(WHISTLE_DICT, WHISTLE_NAME, WHISTLE_MS, 8.0, -8.0);
nextWhistleAt = Date.now() + WHISTLE_MS;
setTimeout(() => {
if (mp.players.local) {
finishConsumableUseAnim(mp.players.local, WHISTLE_DICT, WHISTLE_NAME);
}
}, WHISTLE_MS);
}
} catch (error) {
mp.gui.chat.push('[whistle] ' + error.message);
} finally {
whistleLoading = false;
}
});
mp.events.add('playBongConsumableAnim', () => {
const player = mp.players.local;
if (!player || !beginConsumableUseAnim()) return;
requestAnimDict(BONG_DICT).then((ok) => {
if (!ok) {
itemUseAnimInProgress = false;
return;
}
player.taskPlayAnim(BONG_DICT, BONG_NAME, 8.0, 1.0, BONG_MS, FLAGS_UPPER_BODY, 0, false, false, false);
setTimeout(() => {
finishConsumableUseAnim(player, BONG_DICT, BONG_NAME);
itemUseAnimInProgress = false;
}, BONG_MS);
});
});
mp.events.add('playCigaretteConsumableAnim', () => {
const player = mp.players.local;
if (!player || !beginConsumableUseAnim()) return;
requestAnimDict(BONG_DICT).then((ok) => {
if (!ok) {
itemUseAnimInProgress = false;
return;
}
player.taskPlayAnim(BONG_DICT, BONG_NAME, 8.0, 1.0, BONG_MS, FLAGS_UPPER_BODY, 0, false, false, false);
const rid = player.remoteId;
clearHandProp(rid);
loadModel(CIGARETTE_PROP_HASH, true).then((loaded) => {
if (!loaded || !pedStillValid(player)) return;
try {
const pos = player.position;
const obj = mp.objects.new(CIGARETTE_PROP_HASH, new mp.Vector3(pos.x, pos.y, pos.z + 0.2), { dimension: player.dimension || 0 });
if (!obj) return;
try { obj.notifyStreaming = true; } catch (_e) {}
const attach = () => {
const bone = player.getBoneIndex(57005);
if (bone != null && bone >= 0) {
obj.attachTo(player.handle, bone, CIGARETTE_PROP_OFFSET.x, CIGARETTE_PROP_OFFSET.y, CIGARETTE_PROP_OFFSET.z, CIGARETTE_PROP_ROT.x, CIGARETTE_PROP_ROT.y, CIGARETTE_PROP_ROT.z, false, false, false, false, 2, true);
}
};
[0, 80, 250, 600, 1200].forEach((ms) => setTimeout(attach, ms));
const timerId = setTimeout(() => {
clearHandProp(rid);
}, BONG_MS);
handPropByRemoteId.set(rid, { obj, timerId, attachIntervalId: null });
} catch (_e) {}
});
setTimeout(() => {
finishConsumableUseAnim(player, BONG_DICT, BONG_NAME);
itemUseAnimInProgress = false;
}, BONG_MS);
});
});
mp.events.add('playAlcoConsumableAnim', () => {
playAlcoUseAnim(ALCO_PROP_HASH, ALCO_PROP_OFFSET, ALCO_PROP_ROT, ALCO_DICT, ALCO_NAME, ALCO_MS);
});
function playThermoCupAnim(hash, profile) {
const player = mp.players.local;
if (!player || !beginConsumableUseAnim()) return;
const attach = profile.attach;
requestAnimDict(profile.dict).then((ok) => {
if (!ok) {
itemUseAnimInProgress = false;
return;
}
player.taskPlayAnim(
profile.dict,
profile.name,
8.0,
1.0,
THERMO_MS,
FLAGS_UPPER_BODY,
0,
false,
false,
false
);
setTimeout(() => {
spawnHandPropAttached(
player,
THERMO_MS,
hash,
attach.offset,
attach.rot,
attach.bone != null ? attach.bone : THERMO_HAND_BONE
);
}, 120);
setTimeout(() => {
finishConsumableUseAnim(player, profile.dict, profile.name);
itemUseAnimInProgress = false;
}, THERMO_MS);
});
}
function startThermoCupConsumableAnim() {
resolveThermoPropHash(true).then((hash) => {
if (!hash) {
itemUseAnimInProgress = false;
mp.events.call('notify:client', 2, 'top-mid', 'Модель термокружки не найдена в игре.', 3000);
mp.events.call('spankAlco:reject');
return;
}
playThermoCupAnim(hash, getThermoUseProfile());
});
}
mp.events.add('playThermoCupConsumableAnim', startThermoCupConsumableAnim);
mp.events.add('playThermosConsumableAnim', startThermoCupConsumableAnim);
const remoteAnimEdge = new Map();
function spawnPropForSyncCfg(ped, cfg) {
if (cfg.prop === 'thermo') {
resolveThermoPropHash(false).then((hash) => {
if (!hash) return;
const attach = getThermoAttach();
const doSpawn = () => spawnHandPropAttached(
ped,
cfg.dur,
hash,
attach.offset,
attach.rot,
attach.bone != null ? attach.bone : THERMO_HAND_BONE,
[0, 80, 250]
);
setTimeout(doSpawn, 80);
});
} else if (cfg.prop === 'alco') {
spawnHandPropAttached(ped, cfg.dur, ALCO_PROP_HASH, ALCO_PROP_OFFSET, ALCO_PROP_ROT);
} else if (cfg.prop === 'cigarette') {
spawnHandPropAttached(ped, cfg.dur, CIGARETTE_PROP_HASH, CIGARETTE_PROP_OFFSET, CIGARETTE_PROP_ROT);
}
}
function clearPropForSyncCfg(rid, cfg) {
if (cfg.prop === 'thermo' || cfg.prop === 'alco' || cfg.prop === 'cigarette') {
clearHandProp(rid);
}
}
function syncRemoteConsumableAnims() {
mp.players.forEach((p) => {
if (!mp.players.exists(p) || p === mp.players.local) return;
const rid = p.remoteId;
for (let i = 0; i < SYNC_CONFIG.length; i++) {
const cfg = SYNC_CONFIG[i];
const key = rid + '_' + cfg.varName;
const on = !!p.getVariable(cfg.varName);
const was = !!remoteAnimEdge.get(key);
if (on && !was) {
remoteAnimEdge.set(key, true);
let animDict = cfg.dict;
let animName = cfg.name;
if (cfg.prop === 'thermo') {
const profile = getThermoUseProfile();
animDict = profile.dict;
animName = profile.name;
}
requestAnimDict(animDict).then((ok) => {
if (!ok || !mp.players.exists(p) || !p.getVariable(cfg.varName)) return;
p.taskPlayAnim(animDict, animName, cfg.speedIn, cfg.speedOut, cfg.dur, FLAGS_UPPER_BODY, 0, false, false, false);
if (cfg.prop) spawnPropForSyncCfg(p, cfg);
});
} else if (!on && was) {
remoteAnimEdge.set(key, false);
clearPropForSyncCfg(rid, cfg);
}
}
});
}
mp.events.add('render', syncRemoteConsumableAnims);
mp.events.add('playerReady', () => {
resolveThermoPropHash(true);
loadModel(ALCO_PROP_HASH, true);
loadModel(CIGARETTE_PROP_HASH, true);
requestAnimDict(THERMO_DICT);
requestAnimDict(ALCO_DICT);
requestAnimDict(BONG_DICT);
});
if (typeof global !== 'undefined') {
global.getThermoCupPropModelName = () => thermoPropModelName || THERMO_PROP_MODEL;
global.getThermosPropModelName = () => thermoPropModelName || THERMO_PROP_MODEL;
global.isConsumableUseAnimBusy = () => itemUseAnimInProgress;
global.getThermoAttachLive = () => ({
offset: { ...THERMO_ATTACH.offset },
rot: { ...THERMO_ATTACH.rot },
scale: THERMO_PROP_SCALE_LIVE,
bone: THERMO_ATTACH.bone,
});
}
/** ─── /tprop — редактор посадки термокружки ─────────────────────────────── */
let thermoEditorOn = false;
let thermoEditorStep = 0.005;
let thermoEditorRotStep = 1;
let thermoEditorKeysBound = false;
let thermoEditorRefreshTimer = null;
function thermoEditorChat(msg) {
try { mp.gui.chat.push(String(msg)); } catch (_e) {}
}
function thermoEditorPrint() {
const o = THERMO_ATTACH.offset;
const r = THERMO_ATTACH.rot;
const line =
`offset: { x: ${o.x.toFixed(3)}, y: ${o.y.toFixed(3)}, z: ${o.z.toFixed(3)} }, ` +
`rot: { x: ${r.x.toFixed(1)}, y: ${r.y.toFixed(1)}, z: ${r.z.toFixed(1)} }, ` +
`scale: ${Number(THERMO_PROP_SCALE_LIVE).toFixed(2)}`;
thermoEditorChat('!{#7dffb3}[tprop] ' + line);
try { mp.console.logInfo('[tprop] ' + line); } catch (_e2) {}
}
function thermoEditorReattach() {
const player = mp.players.local;
if (!player) return;
const attach = THERMO_ATTACH;
resolveThermoPropHash(false).then((hash) => {
if (!hash || !thermoEditorOn) return;
spawnHandPropAttached(
player,
60000,
hash,
attach.offset,
attach.rot,
attach.bone,
[0, 40, 120]
);
});
}
function thermoEditorEnsureAnim() {
const player = mp.players.local;
if (!player) return;
requestAnimDict(THERMO_DICT).then((ok) => {
if (!ok || !thermoEditorOn) return;
player.taskPlayAnim(
THERMO_DICT,
THERMO_NAME,
8.0,
1.0,
-1,
FLAGS_UPPER_BODY,
0,
false,
false,
false
);
thermoEditorReattach();
});
}
function thermoEditorStop() {
thermoEditorOn = false;
if (thermoEditorRefreshTimer) {
try { clearInterval(thermoEditorRefreshTimer); } catch (_e) {}
thermoEditorRefreshTimer = null;
}
const player = mp.players.local;
if (player) {
clearHandProp(player.remoteId);
finishConsumableUseAnim(player, THERMO_DICT, THERMO_NAME);
}
thermoEditorChat('!{#cccccc}[tprop] Выкл. Скопируй значения в THERMO_ATTACH в consumables_anim.js');
}
function thermoEditorStart() {
thermoEditorOn = true;
thermoEditorChat('!{#7dffb3}[tprop] ON — стрелки XYZ offset, Q/E rotX, Z/C rotY, R/F rotZ, [/] шаг, -/= scale, Enter=print, Backspace=выкл');
thermoEditorPrint();
thermoEditorEnsureAnim();
if (thermoEditorRefreshTimer) {
try { clearInterval(thermoEditorRefreshTimer); } catch (_e) {}
}
thermoEditorRefreshTimer = setInterval(() => {
if (!thermoEditorOn) return;
thermoEditorEnsureAnim();
}, 4500);
}
function thermoEditorNudge(axis, isRot, dir) {
if (!thermoEditorOn) return;
const step = isRot ? thermoEditorRotStep : thermoEditorStep;
const target = isRot ? THERMO_ATTACH.rot : THERMO_ATTACH.offset;
target[axis] = Number((target[axis] + dir * step).toFixed(isRot ? 1 : 3));
thermoEditorReattach();
thermoEditorPrint();
}
function bindThermoEditorKeys() {
if (thermoEditorKeysBound) return;
thermoEditorKeysBound = true;
const bind = (vk, fn) => {
mp.keys.bind(vk, true, () => {
if (!thermoEditorOn) return;
if (mp.gui.cursor.visible) return;
if (mp.players.local.isTypingInTextChat) return;
fn();
});
};
bind(0x25, () => thermoEditorNudge('x', false, -1));
bind(0x27, () => thermoEditorNudge('x', false, 1));
bind(0x26, () => thermoEditorNudge('y', false, 1));
bind(0x28, () => thermoEditorNudge('y', false, -1));
bind(0x21, () => thermoEditorNudge('z', false, 1));
bind(0x22, () => thermoEditorNudge('z', false, -1));
bind(0x51, () => thermoEditorNudge('x', true, -1));
bind(0x45, () => thermoEditorNudge('x', true, 1));
bind(0x5A, () => thermoEditorNudge('y', true, -1));
bind(0x43, () => thermoEditorNudge('y', true, 1));
bind(0x52, () => thermoEditorNudge('z', true, -1));
bind(0x46, () => thermoEditorNudge('z', true, 1));
bind(0xDB, () => {
thermoEditorStep = Math.max(0.001, Number((thermoEditorStep / 2).toFixed(3)));
thermoEditorRotStep = Math.max(0.5, thermoEditorRotStep / 2);
thermoEditorChat(`!{#cccccc}[tprop] step=${thermoEditorStep} rotStep=${thermoEditorRotStep}`);
});
bind(0xDD, () => {
thermoEditorStep = Math.min(0.05, Number((thermoEditorStep * 2).toFixed(3)));
thermoEditorRotStep = Math.min(15, thermoEditorRotStep * 2);
thermoEditorChat(`!{#cccccc}[tprop] step=${thermoEditorStep} rotStep=${thermoEditorRotStep}`);
});
bind(0xBD, () => {
THERMO_PROP_SCALE_LIVE = Math.max(0.5, Number((THERMO_PROP_SCALE_LIVE - 0.02).toFixed(2)));
thermoEditorReattach();
thermoEditorPrint();
});
bind(0xBB, () => {
THERMO_PROP_SCALE_LIVE = Math.min(2.5, Number((THERMO_PROP_SCALE_LIVE + 0.02).toFixed(2)));
thermoEditorReattach();
thermoEditorPrint();
});
bind(0x0D, () => thermoEditorPrint());
bind(0x08, () => thermoEditorStop());
}
mp.events.add('thermos:propEditor:c', (toggleRaw) => {
try {
if ((mp.players.local.getVariable('adminlvl') || 0) < 1) return;
bindThermoEditorKeys();
const t = String(toggleRaw == null ? '' : toggleRaw).toLowerCase();
if (t === 'off' || t === '0' || t === 'stop') {
thermoEditorStop();
return;
}
if (thermoEditorOn) thermoEditorStop();
else thermoEditorStart();
} catch (_e) {}
});
// ====================== ПЕРЕКЛЮЧЕНИЕ СВИСТА ======================
mp.events.add('toggleWhistleVariant', () => {
selectedWhistle = selectedWhistle === 1 ? 2 : 1;
try {
mp.storage.data.selectedWhistle = selectedWhistle;
mp.storage.flush();
} catch (e) {}
const text = selectedWhistle === 1 ? 'Обычный свист' : 'Новый свист (F4-стиль)';
mp.gui.chat.push(`!{#7dffb3}[Свист] Выбран: ${text}`);
});
// Временно: F7 переключает свист
mp.keys.bind(0x76, true, () => { // F7
if (mp.gui.cursor.visible || mp.players.local.isTypingInTextChat) return;
mp.events.call('toggleWhistleVariant');
});