Add Leonid Horny control, /ostyn cool-down, and /horny-game.

Second persona slider (0-100%) auto-rendered from controls schema; cool-down drops 30 points; horny-game lets the model patch horny from taste match. Fix partial control saves to DeepMerge.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-22 02:33:22 +03:00
co-authored by Cursor
parent 26e4ef76fb
commit fee3eb8965
9 changed files with 143 additions and 17 deletions
+88 -3
View File
@@ -4352,6 +4352,22 @@
action: s.action || '',
}));
}
if (Array.isArray(data.ui?.slash_extra) && data.ui.slash_extra.length) {
for (const s of data.ui.slash_extra) {
const cmd = s.cmd || '';
if (!cmd || SLASH_COMMANDS.some((c) => c.cmd === cmd)) {
continue;
}
SLASH_COMMANDS.push({
cmd,
hint: s.hint || '',
action: s.action || '',
});
}
}
if (data.ui?.help_extra) {
HELP_TEXT = `${HELP_TEXT || ''}\n\n${data.ui.help_extra}`.trim();
}
state.enabledSkills = Array.isArray(data.enabled_skills) ? data.enabled_skills.slice() : [];
if (Array.isArray(data.personas)) {
state.personas = data.personas;
@@ -4414,7 +4430,15 @@
return;
}
box.hidden = false;
for (const id of keys) {
const ordered = keys.slice().sort((a, b) => {
const oa = Number(schema[a]?.order ?? 100);
const ob = Number(schema[b]?.order ?? 100);
if (oa !== ob) {
return oa - ob;
}
return String(a).localeCompare(String(b));
});
for (const id of ordered) {
const def = schema[id];
if (!def || typeof def !== 'object') {
continue;
@@ -4430,6 +4454,8 @@
if (Number.isNaN(cur)) {
cur = defVal;
}
const asPercent = String(def.display || '').toLowerCase() === 'percent';
const fmt = (v) => (asPercent ? `${Math.round(v)}%` : Number(v).toFixed(2));
const row = document.createElement('div');
row.className = 'sa-control-row';
row.title = def.hint || id;
@@ -4444,10 +4470,10 @@
input.dataset.controlId = id;
const valEl = document.createElement('span');
valEl.className = 'sa-control-val';
valEl.textContent = cur.toFixed(2);
valEl.textContent = fmt(cur);
const onInput = () => {
const v = Number(input.value);
valEl.textContent = v.toFixed(2);
valEl.textContent = fmt(v);
if (controlSaveTimer) {
clearTimeout(controlSaveTimer);
}
@@ -4462,6 +4488,57 @@
}
}
function getControlValue(id, fallback) {
const v = state.config?.control_values?.[id] ?? state.exact?.controls?.[id];
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
}
function coolDownHorny() {
const persona = $('sa_persona')?.value || '';
if (persona !== 'leonid') {
setStatus('/остынь только для Leonid');
return;
}
const schema = state.config?.controls || {};
if (!schema.horny) {
setStatus('У этой личности нет слайдера Хорни');
return;
}
const min = Number(schema.horny.min ?? 0);
const max = Number(schema.horny.max ?? 100);
const cur = getControlValue('horny', Number(schema.horny.default ?? 35));
const next = Math.max(min, Math.min(max, cur - 30));
savePersonaControls({ horny: next });
renderPersonaControls(schema, {
...(state.config?.control_values || state.exact?.controls || {}),
horny: next,
});
appendSystemNote(`Хорни: ${Math.round(cur)}% → ${Math.round(next)}% (30)`);
setStatus(`/остынь → ${Math.round(next)}%`);
}
async function startHornyGame() {
const persona = $('sa_persona')?.value || '';
if (persona !== 'leonid') {
setStatus('/horny-game только для Leonid');
return;
}
const cur = getControlValue('horny', 35);
await sendChat({
skipSlash: true,
skipAutoPack: true,
forcedUserText:
`Команда /horny-game. Текущий controls.horny = ${Math.round(cur)} (0100).\n` +
`Оцени, насколько вкусы пользователя в этом чате / последнем сообщении совпадают с твоими (roleplay, outfits, realism, fetishes).\n` +
`Поставь новый controls.horny: умножь/сдвинь текущее значение пропорционально «насколько тебе это зашло» ` +
`(слабое совпадение → чуть вниз или почти без изменений; сильное → заметный рост, clamp 0100).\n` +
`В прозе скажи кратко: совпало ли, какой множитель/сдвиг и новый %. ` +
`Обязателен JSON patch с "controls": { "horny": <number> }. Без generate, если не просили картинку.`,
});
setStatus('/horny-game…');
}
function savePersonaControls(partial) {
const persona = $('sa_persona')?.value || 'neutral';
if (typeof genericRequest !== 'function') {
@@ -6768,6 +6845,14 @@
}
return true;
}
if (cmd === 'остынь' || cmd === 'ostyn' || cmd === 'cool' || cmd === 'cooldown') {
coolDownHorny();
return true;
}
if (cmd === 'horny-game' || cmd === 'hornygame' || cmd === 'horny_game') {
await startHornyGame();
return true;
}
if (cmd === 'civitai') {
if (!arg) {
setStatus('/civitai <query>');