@7ots/cli 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/cli/7ots.mjs +13 -1
- package/cli/lib/account.mjs +152 -2
- package/cli/lib/assistant.mjs +107 -38
- package/cli/lib/brain.mjs +13 -2
- package/cli/lib/config.mjs +5 -1
- package/cli/lib/inferences.mjs +30 -6
- package/cli/lib/integrations.mjs +69 -5
- package/cli/lib/orquesta.mjs +44 -1
- package/cli/lib/pet-server.mjs +352 -11
- package/cli/lib/prompts.mjs +31 -17
- package/cli/lib/prowl.mjs +110 -0
- package/cli/pet/electron/director.cjs +37 -5
- package/cli/pet/electron/main.cjs +92 -10
- package/cli/pet/pet.html +149 -10
- package/cli/pet/settings.html +549 -43
- package/dist/7ots.esm.js +216 -137
- package/dist/7ots.esm.js.map +4 -4
- package/dist/7ots.iife.js +216 -137
- package/dist/7ots.iife.js.map +4 -4
- package/llms.txt +1 -1
- package/package.json +17 -14
- package/server/channels/agent.mjs +7 -1
- package/server/channels/index.mjs +3 -2
- package/server/platform/connect.mjs +4 -1
- package/server/platform/db.mjs +14 -1
- package/server/platform/device.mjs +101 -4
- package/server/platform/mods.mjs +222 -0
- package/server/platform/orquesta.mjs +78 -14
- package/server/platform/profile.mjs +161 -0
- package/server/platform/routes.mjs +39 -33
- package/server/platform/runtime.mjs +5 -3
- package/server/platform/store.mjs +1 -1
- package/server/platform/vm.mjs +688 -17
- package/server/server.mjs +12 -2
- package/server/tts.mjs +17 -4
- package/src/avatar/three.js +3 -2
- package/src/character/AvatarEditor.js +95 -7
- package/src/character/Character.js +116 -17
- package/src/character/TradingCard.js +226 -0
- package/src/character/mods/index.js +4 -0
- package/src/character/mods/orc.js +64 -0
- package/src/character/mods.js +251 -0
- package/src/character/more/acc-head.js +213 -0
- package/src/character/more/acc-rest.js +342 -0
- package/src/character/more/brows.js +39 -0
- package/src/character/more/cheeks.js +78 -0
- package/src/character/more/eyes.js +288 -0
- package/src/character/more/mouths.js +259 -0
- package/src/character/more/patterns.js +120 -0
- package/src/character/parts.js +100 -0
- package/src/character/skin.js +166 -1
- package/src/character/styles.js +1285 -9
- package/src/character3d/Character3D.js +253 -37
- package/src/character3d/index.js +7 -1
- package/src/character3d/model.js +367 -0
- package/src/i18n/messages/character.js +37 -22
- package/src/i18n/messages/platform.js +33 -0
- package/src/identity/random.js +17 -3
- package/src/identity/schema.js +10 -0
- package/src/index.js +1 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mod de cuerpo completo: un modelo 3D (VRM o GLB) en lugar del cuerpo procedural del ot.
|
|
3
|
+
*
|
|
4
|
+
* El modelo vive dentro del mismo `rig` de Character3D, así que gestos sin animación propia,
|
|
5
|
+
* efectos, ánimos y el encuadre siguen funcionando. Lo que el modelo traiga se aprovecha:
|
|
6
|
+
* - VRM (0.x y 1.0): expresiones estándar (aa/ih/ou/ee/oh, blink, happy, angry, sad,
|
|
7
|
+
* relaxed, surprised) y el hueso de la cabeza para mirar.
|
|
8
|
+
* - GLB: morph targets por nombre (blink, jawOpen/mouthOpen, happy/smile, sad, angry,
|
|
9
|
+
* surprised) y un hueso «head» si lo hay.
|
|
10
|
+
* - Animaciones: «idle» en bucle; las que se llamen como un gesto (wave, dance, yes, no,
|
|
11
|
+
* jump, thumbsup…) sustituyen a la pose procedural de ese gesto.
|
|
12
|
+
* Unidades: el modelo se escala a la altura de un ot (su caja), con y hacia arriba y de frente a +z
|
|
13
|
+
* (los VRM 0.x miran a -z y se giran solos).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as THREE from 'three';
|
|
17
|
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
18
|
+
import { layout } from '../character/Character.js';
|
|
19
|
+
import { EASE, clamp, approach } from '../character/motion.js';
|
|
20
|
+
import { U } from './body.js';
|
|
21
|
+
import { shadowTexture } from './materials.js';
|
|
22
|
+
|
|
23
|
+
const DEG = Math.PI / 180;
|
|
24
|
+
const TAU = Math.PI * 2;
|
|
25
|
+
|
|
26
|
+
/** Animación del modelo para cada gesto de 7ots (por nombre del clip). */
|
|
27
|
+
const CLIP_FOR = {
|
|
28
|
+
wave: /wave|hello|greet|hi\b/i,
|
|
29
|
+
dance: /dance/i,
|
|
30
|
+
celebrate: /victory|celebrat|cheer|dance/i,
|
|
31
|
+
jump: /jump/i,
|
|
32
|
+
bounce: /jump|hop/i,
|
|
33
|
+
nod: /\byes\b|nod/i,
|
|
34
|
+
shake: /^no$|\bno\b|shake_?head/i,
|
|
35
|
+
thumbsup: /thumbs?_?up|\byes\b/i,
|
|
36
|
+
thumbsdown: /thumbs?_?down|\bno\b/i,
|
|
37
|
+
bow: /bow/i,
|
|
38
|
+
think: /think/i,
|
|
39
|
+
clap: /clap/i,
|
|
40
|
+
point: /point/i,
|
|
41
|
+
laugh: /laugh/i,
|
|
42
|
+
stomp: /stomp|punch/i,
|
|
43
|
+
shrug: /shrug/i,
|
|
44
|
+
dizzy: /dizz|death|die/i,
|
|
45
|
+
yawn: /yawn|sit/i,
|
|
46
|
+
fly: /fly/i,
|
|
47
|
+
};
|
|
48
|
+
const IDLE = /idle|breath|stand/i;
|
|
49
|
+
|
|
50
|
+
/** Nombre de expresión VRM 0.x → VRM 1.0. */
|
|
51
|
+
const VRM0 = { a: 'aa', i: 'ih', u: 'ou', e: 'ee', o: 'oh', joy: 'happy', sorrow: 'sad', fun: 'relaxed', angry: 'angry', blink: 'blink', surprised: 'surprised' };
|
|
52
|
+
/** Morph targets de un GLB cualquiera → expresión. */
|
|
53
|
+
const MORPH_NAMES = [
|
|
54
|
+
['aa', /jaw_?open|mouth_?open|viseme_?aa|^v_?aa$|^aa$|^a$/i],
|
|
55
|
+
['oh', /viseme_?(oh|o|ou)$|^oh$|^o$|mouth_?(funnel|pucker)/i],
|
|
56
|
+
['blink', /^blink$|blink_?both|eyes?_?clos|^eyeblink$|^blink/i],
|
|
57
|
+
['happy', /happy|joy|smile/i],
|
|
58
|
+
['sad', /sad|sorrow/i],
|
|
59
|
+
['angry', /angry|anger|\bmad\b/i],
|
|
60
|
+
['surprised', /surpris|shock/i],
|
|
61
|
+
];
|
|
62
|
+
/** Ánimo de 7ots → expresiones del modelo (con su peso). */
|
|
63
|
+
const MOOD_EXPR = {
|
|
64
|
+
happy: { happy: 0.8 },
|
|
65
|
+
love: { happy: 0.7, relaxed: 0.3 },
|
|
66
|
+
angry: { angry: 0.85 },
|
|
67
|
+
disgust: { angry: 0.5 },
|
|
68
|
+
sad: { sad: 0.85 },
|
|
69
|
+
fear: { surprised: 0.5, sad: 0.3 },
|
|
70
|
+
surprised: { surprised: 0.9 },
|
|
71
|
+
sleep: { relaxed: 0.6 },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function loadGltf(url) {
|
|
75
|
+
return new Promise((res, rej) => {
|
|
76
|
+
const loader = new GLTFLoader();
|
|
77
|
+
if (url.startsWith('data:')) {
|
|
78
|
+
const bin = atob(url.slice(url.indexOf(',') + 1));
|
|
79
|
+
const buf = new Uint8Array(bin.length);
|
|
80
|
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
81
|
+
loader.parse(buf.buffer, '', res, rej);
|
|
82
|
+
} else loader.load(url, res, undefined, rej);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const meshesUnder = (obj) => {
|
|
87
|
+
const out = [];
|
|
88
|
+
obj.traverse((o) => { if (o.isMesh && o.morphTargetInfluences) out.push(o); });
|
|
89
|
+
return out;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Expresiones del modelo: { nombre: [{ mesh, index, w }] }. */
|
|
93
|
+
async function expressionsOf(gltf) {
|
|
94
|
+
const ex = {};
|
|
95
|
+
const add = (name, mesh, index, w) => { if (Number.isInteger(index)) (ex[name] ||= []).push({ mesh, index, w }); };
|
|
96
|
+
const { parser } = gltf;
|
|
97
|
+
const ext = parser.json.extensions || {};
|
|
98
|
+
if (ext.VRMC_vrm) {
|
|
99
|
+
const all = { ...ext.VRMC_vrm.expressions?.preset, ...ext.VRMC_vrm.expressions?.custom };
|
|
100
|
+
for (const [name, e] of Object.entries(all)) {
|
|
101
|
+
for (const b of e?.morphTargetBinds || []) {
|
|
102
|
+
const node = await parser.getDependency('node', b.node).catch(() => null);
|
|
103
|
+
if (node) for (const m of meshesUnder(node)) add(name, m, b.index, b.weight ?? 1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else if (ext.VRM) {
|
|
107
|
+
const byMesh = new Map();
|
|
108
|
+
gltf.scene.traverse((o) => {
|
|
109
|
+
const a = parser.associations.get(o);
|
|
110
|
+
if (o.isMesh && a?.meshes != null) (byMesh.get(a.meshes) || byMesh.set(a.meshes, []).get(a.meshes)).push(o);
|
|
111
|
+
});
|
|
112
|
+
for (const g of ext.VRM.blendShapeMaster?.blendShapeGroups || []) {
|
|
113
|
+
const name = VRM0[String(g.presetName || '').toLowerCase()];
|
|
114
|
+
if (!name) continue;
|
|
115
|
+
for (const b of g.binds || []) for (const m of byMesh.get(b.mesh) || []) add(name, m, b.index, (b.weight ?? 100) / 100);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
gltf.scene.traverse((o) => {
|
|
119
|
+
if (!o.isMesh || !o.morphTargetDictionary) return;
|
|
120
|
+
for (const [key, index] of Object.entries(o.morphTargetDictionary)) {
|
|
121
|
+
const hit = MORPH_NAMES.find(([, re]) => re.test(key));
|
|
122
|
+
if (hit) add(hit[0], o, index, 1);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return ex;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Hueso de la cabeza (VRM humanoid, o el primer hueso «head»). */
|
|
130
|
+
async function headOf(gltf) {
|
|
131
|
+
const { parser } = gltf;
|
|
132
|
+
const ext = parser.json.extensions || {};
|
|
133
|
+
const idx = ext.VRMC_vrm?.humanoid?.humanBones?.head?.node ?? ext.VRM?.humanoid?.humanBones?.find((b) => b.bone === 'head')?.node;
|
|
134
|
+
if (idx != null) return parser.getDependency('node', idx).catch(() => null);
|
|
135
|
+
let head = null;
|
|
136
|
+
const re = /^(mixamorig:?)?head$|^head[_\d]*$/i;
|
|
137
|
+
gltf.scene.traverse((o) => { if (!head && o.isBone && re.test(o.name)) head = o; });
|
|
138
|
+
// sin esqueleto: un nodo «head» rígido (modelos hechos por partes) también sirve
|
|
139
|
+
if (!head) gltf.scene.traverse((o) => { if (!head && !o.isMesh && re.test(o.name)) head = o; });
|
|
140
|
+
return head;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Construye el ot como modelo (llamado desde Character3D.build con la ficha ya resuelta).
|
|
145
|
+
* `this` es la instancia de Character3D.
|
|
146
|
+
*/
|
|
147
|
+
export function buildModel(s) {
|
|
148
|
+
const m = s._model;
|
|
149
|
+
const L = (this.L = layout(s));
|
|
150
|
+
const W = (this.W = L.box.w * U);
|
|
151
|
+
const H = (this.H = Math.max(L.box.h * U, W) * 1.2);
|
|
152
|
+
this.modelMode = true;
|
|
153
|
+
this.kk = Math.sqrt(W * H) / 1.25;
|
|
154
|
+
this.depth = W * 0.6;
|
|
155
|
+
this.furLen = 0;
|
|
156
|
+
this.plush = false;
|
|
157
|
+
this.maxShells = 0;
|
|
158
|
+
this.pal = { body: s.body.color };
|
|
159
|
+
this.furs = [];
|
|
160
|
+
this.parts = [];
|
|
161
|
+
this.anims = [];
|
|
162
|
+
this.bodyMeshes = [];
|
|
163
|
+
this.eyes = [];
|
|
164
|
+
this.brows = [];
|
|
165
|
+
this.hands = [];
|
|
166
|
+
this.eyeY = H * 0.86;
|
|
167
|
+
this.r3 = W * 0.07;
|
|
168
|
+
this.model = { ex: {}, w: {}, head: null, q0: null, mixer: null, clips: [], idle: null, act: null, ready: false };
|
|
169
|
+
this.fxc = { W, H, D: this.depth, eyeY: this.eyeY, r: this.r3, sp: W * 0.12, mouth: new THREE.Vector3(0, H * 0.78, this.depth * 0.5), low: this.quality === 'low', tears: [] };
|
|
170
|
+
|
|
171
|
+
const sh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: shadowTexture(), transparent: true, depthWrite: false, toneMapped: false }));
|
|
172
|
+
sh.rotation.x = -Math.PI / 2;
|
|
173
|
+
sh.position.y = 0.002;
|
|
174
|
+
sh.scale.set(W * 1.2, this.depth * 2, 1);
|
|
175
|
+
sh.renderOrder = -1;
|
|
176
|
+
this.shadow = sh;
|
|
177
|
+
this.root.add(sh);
|
|
178
|
+
|
|
179
|
+
// caja provisional mientras carga (el encuadre se rehace al llegar el modelo)
|
|
180
|
+
this.bounds = new THREE.Box3(new THREE.Vector3(-W / 2, 0, -this.depth / 2), new THREE.Vector3(W / 2, H, this.depth / 2));
|
|
181
|
+
this.boundsRev = (this.boundsRev || 0) + 1;
|
|
182
|
+
|
|
183
|
+
const tok = (this.modelTok = {});
|
|
184
|
+
loadGltf(m.url)
|
|
185
|
+
.then(async (gltf) => {
|
|
186
|
+
if (this.modelTok !== tok) return;
|
|
187
|
+
const scene = gltf.scene;
|
|
188
|
+
const ext = gltf.parser.json.extensions || {};
|
|
189
|
+
if (ext.VRM) scene.rotation.y = Math.PI; // VRM 0.x mira a -z
|
|
190
|
+
scene.traverse((o) => {
|
|
191
|
+
if (!o.isMesh) return;
|
|
192
|
+
o.frustumCulled = false;
|
|
193
|
+
if (!Array.isArray(o.material)) o.userData.ownMaterial = true;
|
|
194
|
+
});
|
|
195
|
+
scene.updateMatrixWorld(true);
|
|
196
|
+
const box = new THREE.Box3().setFromObject(scene);
|
|
197
|
+
const size = box.getSize(new THREE.Vector3());
|
|
198
|
+
const c = box.getCenter(new THREE.Vector3());
|
|
199
|
+
if (!(size.y > 0)) throw new Error('empty model');
|
|
200
|
+
scene.position.set(-c.x, -box.min.y, -c.z);
|
|
201
|
+
const holder = new THREE.Group();
|
|
202
|
+
holder.add(scene);
|
|
203
|
+
const k = (H / size.y) * m.scale;
|
|
204
|
+
holder.scale.setScalar(k);
|
|
205
|
+
holder.rotation.y = m.rot * DEG;
|
|
206
|
+
holder.position.y = m.y * H;
|
|
207
|
+
this.rig.add(holder);
|
|
208
|
+
|
|
209
|
+
const md = this.model;
|
|
210
|
+
md.ex = await expressionsOf(gltf);
|
|
211
|
+
md.head = await headOf(gltf);
|
|
212
|
+
if (this.modelTok !== tok) return;
|
|
213
|
+
if (md.head) md.q0 = md.head.quaternion.clone();
|
|
214
|
+
if (gltf.animations.length) {
|
|
215
|
+
md.mixer = new THREE.AnimationMixer(scene);
|
|
216
|
+
md.clips = gltf.animations;
|
|
217
|
+
const idle = md.clips.find((a) => IDLE.test(a.name));
|
|
218
|
+
if (idle) {
|
|
219
|
+
md.idle = md.mixer.clipAction(idle);
|
|
220
|
+
md.idle.play();
|
|
221
|
+
}
|
|
222
|
+
md.mixer.addEventListener('finished', (e) => {
|
|
223
|
+
if (e.action !== md.act) return;
|
|
224
|
+
md.act = null;
|
|
225
|
+
e.action.fadeOut(0.25);
|
|
226
|
+
md.idle?.reset().fadeIn(0.25).play();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
md.ready = true;
|
|
230
|
+
|
|
231
|
+
// encuadre y puntos de los efectos con el modelo real
|
|
232
|
+
this.rig.updateMatrixWorld(true);
|
|
233
|
+
const rb = new THREE.Box3().setFromObject(holder);
|
|
234
|
+
this.bounds = rb;
|
|
235
|
+
this.depth = Math.max(0.05, rb.max.z - rb.min.z);
|
|
236
|
+
this.W = Math.max(0.05, rb.max.x - rb.min.x);
|
|
237
|
+
this.shadow.scale.set(this.W * 1.1, this.depth * 1.6, 1);
|
|
238
|
+
if (md.head) {
|
|
239
|
+
const hp = md.head.getWorldPosition(new THREE.Vector3());
|
|
240
|
+
this.rig.worldToLocal(hp);
|
|
241
|
+
this.eyeY = this.fxc.eyeY = hp.y + H * 0.04;
|
|
242
|
+
this.fxc.mouth.set(hp.x, hp.y - H * 0.01, rb.max.z * 0.6);
|
|
243
|
+
}
|
|
244
|
+
this.fxc.W = this.W;
|
|
245
|
+
this.fxc.D = this.depth;
|
|
246
|
+
this.boundsRev++;
|
|
247
|
+
})
|
|
248
|
+
.catch((e) => {
|
|
249
|
+
if (this.modelTok !== tok) return;
|
|
250
|
+
console.warn('[7ots] modelo 3D del mod no disponible:', e?.message || e);
|
|
251
|
+
// sin modelo: un ot normal (la ficha sin el modelo)
|
|
252
|
+
const { _model, ...rest } = s;
|
|
253
|
+
this.modelFailed = m.url;
|
|
254
|
+
this.build({ ...rest, mods: (rest.mods || []).filter((x) => !x.model) });
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Gesto con la animación propia del modelo, si la trae. Devuelve true si la usó. */
|
|
259
|
+
export function modelGesture(name) {
|
|
260
|
+
const md = this.model;
|
|
261
|
+
if (!md?.mixer) return false;
|
|
262
|
+
const re = CLIP_FOR[name];
|
|
263
|
+
const clip = re && md.clips.find((a) => re.test(a.name) && !IDLE.test(a.name));
|
|
264
|
+
if (!clip) return false;
|
|
265
|
+
const a = md.mixer.clipAction(clip);
|
|
266
|
+
if (md.act && md.act !== a) md.act.fadeOut(0.2);
|
|
267
|
+
a.reset();
|
|
268
|
+
a.setLoop(THREE.LoopOnce, 1);
|
|
269
|
+
a.clampWhenFinished = false;
|
|
270
|
+
a.fadeIn(0.2).play();
|
|
271
|
+
md.idle?.fadeOut(0.2);
|
|
272
|
+
md.act = a;
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Un frame del ot-modelo (equivale a Character3D.tick). */
|
|
277
|
+
export function tickModel(t, dt) {
|
|
278
|
+
const rm = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
279
|
+
const idle = this.opts.idle !== false;
|
|
280
|
+
const md = this.model;
|
|
281
|
+
if (idle && t >= this.nextBlink) {
|
|
282
|
+
if (this.moodName !== 'sleep' && !rm) {
|
|
283
|
+
this.blinkAt = t;
|
|
284
|
+
if (t - this.look.at > 3000 && Math.random() < 0.45) {
|
|
285
|
+
this.look.tx = Math.random() < 0.5 ? 0 : (Math.random() - 0.5) * 0.9;
|
|
286
|
+
this.look.ty = Math.random() < 0.5 ? 0 : (Math.random() - 0.5) * 0.5;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
this.nextBlink = t + 1800 + Math.random() * 3600;
|
|
290
|
+
}
|
|
291
|
+
if (this.speech && !this.speech.tick(t)) this.endSpeech();
|
|
292
|
+
const mo = this.mo;
|
|
293
|
+
mo.open = approach(mo.open, mo.target, dt, mo.target > mo.open ? 35 : 70);
|
|
294
|
+
mo.msx = approach(mo.msx, mo.tmsx, dt, 60);
|
|
295
|
+
const look = this.look;
|
|
296
|
+
look.x = rm ? look.tx : approach(look.x, look.tx, dt, 70);
|
|
297
|
+
look.y = rm ? look.ty : approach(look.y, look.ty, dt, 70);
|
|
298
|
+
this.sleepK = approach(this.sleepK, this.moodName === 'sleep' ? 1 : 0, dt, rm ? 1 : 260);
|
|
299
|
+
|
|
300
|
+
// gesto: la animación del modelo, o la pose procedural sobre todo el cuerpo
|
|
301
|
+
const pose = { x: 0, y: 0, r: 0, sx: 1, sy: 1, blink: 0 };
|
|
302
|
+
let spin = 0;
|
|
303
|
+
const gest = this.gest;
|
|
304
|
+
if (gest) {
|
|
305
|
+
const age = t - gest.start;
|
|
306
|
+
const tt = clamp(age / gest.ms);
|
|
307
|
+
if (gest.clip === undefined) gest.clip = md.ready && this.modelGesture(gest.name);
|
|
308
|
+
if (!gest.clip) {
|
|
309
|
+
Object.assign(pose, gest.def.pose(tt, rm ? 0 : age, gest));
|
|
310
|
+
if (gest.dir < 0) { pose.x = -pose.x; pose.r = -pose.r; }
|
|
311
|
+
pose.sx = 1 + (pose.sx - 1) * 0.4; // un muñeco se aplasta menos que un ot
|
|
312
|
+
pose.sy = 1 + (pose.sy - 1) * 0.4;
|
|
313
|
+
if (gest.name === 'spin') spin = EASE.inOut(clamp((tt - 0.12) / 0.76)) * TAU;
|
|
314
|
+
}
|
|
315
|
+
for (const f of gest.def.fx || []) {
|
|
316
|
+
if (tt >= f[0] && !gest.fired.has(f)) {
|
|
317
|
+
gest.fired.add(f);
|
|
318
|
+
this.effect(f[1], { dir: gest.dir, ...(f[2] || {}) });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (tt >= 1) this.endGesture();
|
|
322
|
+
}
|
|
323
|
+
if (rm) Object.assign(pose, { x: 0, y: 0, r: 0, sx: 1, sy: 1 });
|
|
324
|
+
const { ksx, ksy } = this.tickMorphs(t, rm);
|
|
325
|
+
|
|
326
|
+
const W = this.W;
|
|
327
|
+
const H = this.H;
|
|
328
|
+
const sec = t / 1000;
|
|
329
|
+
const breath = rm || !idle || md.idle ? 0 : Math.sin(sec * TAU / 3.4 + this.phase);
|
|
330
|
+
const rig = this.rig;
|
|
331
|
+
rig.position.set(pose.x * W, -pose.y * H, 0);
|
|
332
|
+
rig.rotation.set(md.head ? 0 : look.y * 0.1, this.yaw + look.x * (md.head ? 0.12 : 0.3) + spin + (rm || !idle ? 0 : Math.sin(sec * 0.7 + this.phase) * 0.03), -(pose.r + look.x * 1.5) * DEG);
|
|
333
|
+
rig.scale.set(pose.sx * ksx * (1 - 0.004 * breath), pose.sy * ksy * (1 + 0.01 * breath), pose.sx * ksx);
|
|
334
|
+
const air = Math.max(0, -pose.y);
|
|
335
|
+
this.shadow.material.opacity = 1 - air * 1.5;
|
|
336
|
+
|
|
337
|
+
// animaciones y cabeza
|
|
338
|
+
if (md.head && md.q0) md.head.quaternion.copy(md.q0);
|
|
339
|
+
md.mixer?.update(rm ? 0 : dt / 1000);
|
|
340
|
+
if (md.head && md.head.parent) {
|
|
341
|
+
const pw = md.head.parent.getWorldQuaternion(new THREE.Quaternion());
|
|
342
|
+
const rw = rig.getWorldQuaternion(new THREE.Quaternion());
|
|
343
|
+
const d = new THREE.Quaternion().setFromEuler(new THREE.Euler(look.y * 0.35, look.x * 0.55, 0, 'YXZ'));
|
|
344
|
+
d.premultiply(rw).multiply(rw.clone().invert()); // giro en el espacio del ot
|
|
345
|
+
md.head.quaternion.premultiply(pw.clone().invert().multiply(d).multiply(pw));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// expresiones
|
|
349
|
+
const blink = Math.max(this.blinkValue(t), pose.blink, this.sleepK > 0.02 ? Math.min(1, this.sleepK * 1.1) : 0);
|
|
350
|
+
const round = clamp((1 - mo.msx) / 0.35);
|
|
351
|
+
const want = { ...(MOOD_EXPR[this.moodName] || {}) };
|
|
352
|
+
for (const k in md.w) if (!(k in want)) want[k] = 0;
|
|
353
|
+
for (const k in want) md.w[k] = rm ? want[k] : approach(md.w[k] || 0, want[k], dt, 120);
|
|
354
|
+
const val = { ...md.w, blink: Math.max(blink, md.w.blink || 0), aa: mo.open * (md.ex.oh ? 1 - round : 1), oh: mo.open * round };
|
|
355
|
+
if (val.happy > 0.3) val.blink = Math.max(val.blink * (1 - val.happy), 0); // VRM: happy ya entrecierra los ojos
|
|
356
|
+
const touched = new Set();
|
|
357
|
+
for (const name in md.ex) for (const b of md.ex[name]) if (!touched.has(b)) { b.mesh.morphTargetInfluences[b.index] = 0; touched.add(b); }
|
|
358
|
+
for (const name in md.ex) {
|
|
359
|
+
const v = val[name] || 0;
|
|
360
|
+
if (!v) continue;
|
|
361
|
+
for (const b of md.ex[name]) b.mesh.morphTargetInfluences[b.index] = Math.min(1, b.mesh.morphTargetInfluences[b.index] + v * b.w);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
this.tickEffects(t, rm);
|
|
365
|
+
this.fxg.position.copy(rig.position);
|
|
366
|
+
this.fxg.rotation.y = this.yaw;
|
|
367
|
+
}
|
|
@@ -45,6 +45,7 @@ const es = {
|
|
|
45
45
|
party: 'Fiesta', chef: 'Chef', viking: 'Vikingo', grad: 'Graduación', beret: 'Boina', bow: 'Lazo', flower: 'Flor', halo: 'Aureola', horns: 'Cuernos',
|
|
46
46
|
antenna: 'Antenas', bunny: 'Orejas de conejo', catears: 'Orejas de gato', santa: 'Papá Noel', hardhat: 'Casco de obra', headband: 'Cinta ninja',
|
|
47
47
|
bandana: 'Pañuelo pirata', helmet: 'Casco espacial', headphones: 'Auriculares', hair: 'Tupé', sprout: 'Brote',
|
|
48
|
+
fedora: 'Fedora', bowler: 'Bombín', propeller: 'Gorra de hélice', witch: 'Sombrero de bruja', fez: 'Fez', sombrero: 'Sombrero mexicano', bucket: 'Gorro pescador', jester: 'Bufón',
|
|
48
49
|
glasses: 'Gafas redondas', square: 'Gafas cuadradas', nerd: 'Gafas empollón', sunglasses: 'Gafas de sol', aviator: 'Aviador', heartglasses: 'Gafas corazón',
|
|
49
50
|
starglasses: 'Gafas estrella', '3d': 'Gafas 3D', monocle: 'Monóculo', eyepatch: 'Parche', goggles: 'Gafas de piloto', visor: 'Visor',
|
|
50
51
|
mustache: 'Bigote', handlebar: 'Bigote manillar', beard: 'Barba', wizardbeard: 'Barba de mago', goatee: 'Perilla', pipe: 'Pipa', lollipop: 'Piruleta',
|
|
@@ -73,7 +74,7 @@ const es = {
|
|
|
73
74
|
},
|
|
74
75
|
},
|
|
75
76
|
editor: {
|
|
76
|
-
kinds: { label: 'Tipo de avatar', character: 'Personaje 2D', plush: 'Personaje 3D', image: '
|
|
77
|
+
kinds: { label: 'Tipo de avatar', character: 'Personaje 2D', plush: 'Personaje 3D', image: 'Carta' },
|
|
77
78
|
tabs: { styles: 'Estilos', shape: 'Forma', color: 'Color y acabado', eyes: 'Ojos', mouth: 'Boca', face: 'Cara', acc: 'Accesorios' },
|
|
78
79
|
controls: 'Controles de edición',
|
|
79
80
|
preview: 'Vista previa del avatar',
|
|
@@ -101,15 +102,19 @@ const es = {
|
|
|
101
102
|
unavailable: 'Este navegador no puede mostrar el 3D; ves la vista 2D (el widget hará lo mismo).',
|
|
102
103
|
},
|
|
103
104
|
image: {
|
|
104
|
-
title: '
|
|
105
|
-
intro: '
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
title: 'Carta coleccionable',
|
|
106
|
+
intro: 'La carta holográfica de tu ot, con su personaje, nombre, rasgos y rareza. Muévela con el puntero.',
|
|
107
|
+
foil: 'Lámina',
|
|
108
|
+
foils: { auto: 'Según rareza', holo: 'Holo', prism: 'Prisma', cosmos: 'Cosmos', gold: 'Oro', none: 'Sin lámina' },
|
|
109
|
+
url: 'Arte propio (opcional)',
|
|
110
|
+
hint: 'Si pones una imagen (PNG, JPG, WebP o SVG), reemplaza al personaje en la carta y como avatar.',
|
|
111
|
+
placeholder: 'https://…/arte.png',
|
|
108
112
|
alt: 'Vista previa del retrato',
|
|
109
113
|
},
|
|
110
114
|
styles: {
|
|
111
|
-
title: '
|
|
112
|
-
hint: '
|
|
115
|
+
title: 'Ots al azar sin dueño',
|
|
116
|
+
hint: 'Caras que nadie tiene todavía. Elegir una reemplaza el personaje; después puedes ajustar cada pieza.',
|
|
117
|
+
more: 'Otros',
|
|
113
118
|
keepBody: 'Conservar mi forma y color al cambiar de estilo',
|
|
114
119
|
},
|
|
115
120
|
shape: {
|
|
@@ -128,7 +133,7 @@ const es = {
|
|
|
128
133
|
custom: 'Personalizado',
|
|
129
134
|
finish: 'Acabado',
|
|
130
135
|
outline: 'Contorno',
|
|
131
|
-
outlines: { auto: 'Automático', none: 'Sin contorno', thin: 'Fino', bold: 'Grueso' },
|
|
136
|
+
outlines: { auto: 'Automático', none: 'Sin contorno', thin: 'Fino', bold: 'Grueso', tone: 'Tono', sticker: 'Pegatina' },
|
|
132
137
|
pattern: 'Patrón',
|
|
133
138
|
patternColor: 'Color del patrón',
|
|
134
139
|
},
|
|
@@ -214,6 +219,7 @@ const en = {
|
|
|
214
219
|
party: 'Party', chef: 'Chef', viking: 'Viking', grad: 'Graduation', beret: 'Beret', bow: 'Bow', flower: 'Flower', halo: 'Halo', horns: 'Horns',
|
|
215
220
|
antenna: 'Antennae', bunny: 'Bunny ears', catears: 'Cat ears', santa: 'Santa', hardhat: 'Hard hat', headband: 'Ninja headband',
|
|
216
221
|
bandana: 'Pirate bandana', helmet: 'Space helmet', headphones: 'Headphones', hair: 'Quiff', sprout: 'Sprout',
|
|
222
|
+
fedora: 'Fedora', bowler: 'Bowler hat', propeller: 'Propeller cap', witch: 'Witch hat', fez: 'Fez', sombrero: 'Sombrero', bucket: 'Bucket hat', jester: 'Jester hat',
|
|
217
223
|
glasses: 'Round glasses', square: 'Square glasses', nerd: 'Nerd glasses', sunglasses: 'Sunglasses', aviator: 'Aviator', heartglasses: 'Heart glasses',
|
|
218
224
|
starglasses: 'Star glasses', '3d': '3D glasses', monocle: 'Monocle', eyepatch: 'Eye patch', goggles: 'Pilot goggles', visor: 'Visor',
|
|
219
225
|
mustache: 'Mustache', handlebar: 'Handlebar mustache', beard: 'Beard', wizardbeard: 'Wizard beard', goatee: 'Goatee', pipe: 'Pipe', lollipop: 'Lollipop',
|
|
@@ -270,15 +276,19 @@ const en = {
|
|
|
270
276
|
unavailable: 'This browser can’t show 3D; you see the 2D preview (the widget will do the same).',
|
|
271
277
|
},
|
|
272
278
|
image: {
|
|
273
|
-
title: '
|
|
274
|
-
intro: '
|
|
275
|
-
|
|
276
|
-
|
|
279
|
+
title: 'Trading card',
|
|
280
|
+
intro: 'Your ot’s holographic card, with its character, name, traits and rarity. Move your pointer over it.',
|
|
281
|
+
foil: 'Foil',
|
|
282
|
+
foils: { auto: 'By rarity', holo: 'Holo', prism: 'Prism', cosmos: 'Cosmos', gold: 'Gold', none: 'No foil' },
|
|
283
|
+
url: 'Custom art (optional)',
|
|
284
|
+
hint: 'An image (PNG, JPG, WebP or SVG) replaces the character on the card and as the avatar.',
|
|
285
|
+
placeholder: 'https://…/art.png',
|
|
277
286
|
alt: 'Portrait preview',
|
|
278
287
|
},
|
|
279
288
|
styles: {
|
|
280
|
-
title: '
|
|
281
|
-
hint: '
|
|
289
|
+
title: 'Random unclaimed ots',
|
|
290
|
+
hint: 'Faces nobody has yet. Picking one replaces the character; you can then adjust each part.',
|
|
291
|
+
more: 'Shuffle',
|
|
282
292
|
keepBody: 'Keep my shape and color when changing style',
|
|
283
293
|
},
|
|
284
294
|
shape: {
|
|
@@ -297,7 +307,7 @@ const en = {
|
|
|
297
307
|
custom: 'Custom',
|
|
298
308
|
finish: 'Finish',
|
|
299
309
|
outline: 'Outline',
|
|
300
|
-
outlines: { auto: 'Automatic', none: 'No outline', thin: 'Thin', bold: 'Bold' },
|
|
310
|
+
outlines: { auto: 'Automatic', none: 'No outline', thin: 'Thin', bold: 'Bold', tone: 'Tone', sticker: 'Sticker' },
|
|
301
311
|
pattern: 'Pattern',
|
|
302
312
|
patternColor: 'Pattern color',
|
|
303
313
|
},
|
|
@@ -383,6 +393,7 @@ const pt = {
|
|
|
383
393
|
party: 'Festa', chef: 'Chef', viking: 'Viking', grad: 'Formatura', beret: 'Boina', bow: 'Laço', flower: 'Flor', halo: 'Auréola', horns: 'Chifres',
|
|
384
394
|
antenna: 'Antenas', bunny: 'Orelhas de coelho', catears: 'Orelhas de gato', santa: 'Papai Noel', hardhat: 'Capacete de obra', headband: 'Faixa ninja',
|
|
385
395
|
bandana: 'Bandana pirata', helmet: 'Capacete espacial', headphones: 'Fones de ouvido', hair: 'Topete', sprout: 'Broto',
|
|
396
|
+
fedora: 'Fedora', bowler: 'Chapéu-coco', propeller: 'Boné de hélice', witch: 'Chapéu de bruxa', fez: 'Fez', sombrero: 'Sombreiro mexicano', bucket: 'Chapéu de pescador', jester: 'Chapéu de bobo da corte',
|
|
386
397
|
glasses: 'Óculos redondos', square: 'Óculos quadrados', nerd: 'Óculos de nerd', sunglasses: 'Óculos de sol', aviator: 'Aviador', heartglasses: 'Óculos de coração',
|
|
387
398
|
starglasses: 'Óculos de estrela', '3d': 'Óculos 3D', monocle: 'Monóculo', eyepatch: 'Tapa-olho', goggles: 'Óculos de piloto', visor: 'Viseira',
|
|
388
399
|
mustache: 'Bigode', handlebar: 'Bigode guidão', beard: 'Barba', wizardbeard: 'Barba de mago', goatee: 'Cavanhaque', pipe: 'Cachimbo', lollipop: 'Pirulito',
|
|
@@ -439,15 +450,19 @@ const pt = {
|
|
|
439
450
|
unavailable: 'Este navegador não consegue mostrar o 3D; você vê a prévia 2D (o widget fará o mesmo).',
|
|
440
451
|
},
|
|
441
452
|
image: {
|
|
442
|
-
title: '
|
|
443
|
-
intro: '
|
|
444
|
-
|
|
445
|
-
|
|
453
|
+
title: 'Carta colecionável',
|
|
454
|
+
intro: 'A carta holográfica do seu ot, com personagem, nome, atributos e raridade. Passe o ponteiro sobre ela.',
|
|
455
|
+
foil: 'Lâmina',
|
|
456
|
+
foils: { auto: 'Pela raridade', holo: 'Holo', prism: 'Prisma', cosmos: 'Cosmos', gold: 'Ouro', none: 'Sem lâmina' },
|
|
457
|
+
url: 'Arte própria (opcional)',
|
|
458
|
+
hint: 'Uma imagem (PNG, JPG, WebP ou SVG) substitui o personagem na carta e como avatar.',
|
|
459
|
+
placeholder: 'https://…/arte.png',
|
|
446
460
|
alt: 'Prévia do retrato',
|
|
447
461
|
},
|
|
448
462
|
styles: {
|
|
449
|
-
title: '
|
|
450
|
-
hint: '
|
|
463
|
+
title: 'Ots aleatórios sem dono',
|
|
464
|
+
hint: 'Rostos que ninguém tem ainda. Escolher um substitui o personagem; depois você pode ajustar cada peça.',
|
|
465
|
+
more: 'Outros',
|
|
451
466
|
keepBody: 'Manter minha forma e cor ao trocar de estilo',
|
|
452
467
|
},
|
|
453
468
|
shape: {
|
|
@@ -466,7 +481,7 @@ const pt = {
|
|
|
466
481
|
custom: 'Personalizada',
|
|
467
482
|
finish: 'Acabamento',
|
|
468
483
|
outline: 'Contorno',
|
|
469
|
-
outlines: { auto: 'Automático', none: 'Sem contorno', thin: 'Fino', bold: 'Grosso' },
|
|
484
|
+
outlines: { auto: 'Automático', none: 'Sem contorno', thin: 'Fino', bold: 'Grosso', tone: 'Tom', sticker: 'Adesivo' },
|
|
470
485
|
pattern: 'Padrão',
|
|
471
486
|
patternColor: 'Cor do padrão',
|
|
472
487
|
},
|
|
@@ -74,6 +74,7 @@ const es = {
|
|
|
74
74
|
busy: 'Ya hay una tarea en marcha ({id}); espera a que acabe',
|
|
75
75
|
noAgent: 'Ese proyecto de Orquesta no tiene agente: configura uno en Orquesta ({url})',
|
|
76
76
|
noProject: 'Ese proyecto de Orquesta no existe o no está compartido con 7ots',
|
|
77
|
+
notLinked: 'Este ot no está conectado a ese proyecto en Orquesta, o no tiene permiso para ejecutar tareas ahí. Conéctalo en el proyecto → Identidad → Ots conectados: {url}',
|
|
77
78
|
rateLimited: 'Orquesta está recibiendo muchas peticiones; espera un momento',
|
|
78
79
|
submitFailed: 'Orquesta no aceptó la tarea (error {status})',
|
|
79
80
|
agentOffline: 'El agente de este proyecto está desconectado: la tarea queda en cola y empezará cuando vuelva',
|
|
@@ -95,6 +96,16 @@ const es = {
|
|
|
95
96
|
jobGone: 'Ese trabajo ya no existe o ya terminó',
|
|
96
97
|
llmLimit: 'Este trabajo ya usó sus {max} llamadas al modelo',
|
|
97
98
|
paidSoon: 'Los servidores de 7ots llegan pronto; mientras tanto usa tu propio servidor',
|
|
99
|
+
paidOff: 'Los servidores de 7ots aún no están activados para tu cuenta; mientras tanto usa tu propio servidor',
|
|
100
|
+
paidUnavailable: 'Los servidores de 7ots no están disponibles ahora mismo; inténtalo más tarde',
|
|
101
|
+
exists: 'Este ot ya tiene un servidor; elimínalo o revócalo antes de crear otro',
|
|
102
|
+
accountLimit: 'Tu cuenta ya tiene {max} servidor(es) de 7ots (su límite)',
|
|
103
|
+
full: 'No quedan servidores de 7ots disponibles por ahora; inténtalo más tarde',
|
|
104
|
+
badSize: 'Tamaño de servidor no disponible',
|
|
105
|
+
needsPayment: 'Falta el pago del servidor de 7ots: vuelve a pulsar «Pagar y crear servidor»',
|
|
106
|
+
userNeedsKey: 'Para crear el servidor en tu cuenta de Orquesta, conéctala con una clave oak_ que tenga los permisos projects:write y vms:write (Orquesta → Ajustes → API keys). La conexión por OAuth no puede crear servidores.',
|
|
107
|
+
userScopes: 'Tu cuenta de Orquesta no dejó crear el servidor: vuelve a conectarla con una clave oak_ que tenga los permisos projects:write y vms:write (Orquesta → Ajustes → API keys)',
|
|
108
|
+
subscriptionInUse: 'Elimina antes tus servidores de 7ots; luego puedes cancelar la suscripción',
|
|
98
109
|
},
|
|
99
110
|
},
|
|
100
111
|
};
|
|
@@ -167,6 +178,7 @@ const en = {
|
|
|
167
178
|
busy: 'A task is already running ({id}); wait for it to finish',
|
|
168
179
|
noAgent: 'That Orquesta project has no agent: set up an agent in Orquesta ({url})',
|
|
169
180
|
noProject: 'That Orquesta project does not exist or is not shared with 7ots',
|
|
181
|
+
notLinked: 'This ot is not connected to that Orquesta project, or may not run tasks there. Connect it in the project → Identity → Connected ots: {url}',
|
|
170
182
|
rateLimited: 'Orquesta is getting too many requests; wait a moment',
|
|
171
183
|
submitFailed: 'Orquesta did not accept the task (error {status})',
|
|
172
184
|
agentOffline: "This project's agent is offline: the task is queued and will start when it is back",
|
|
@@ -188,6 +200,16 @@ const en = {
|
|
|
188
200
|
jobGone: 'That job no longer exists or already finished',
|
|
189
201
|
llmLimit: 'This job already used its {max} model calls',
|
|
190
202
|
paidSoon: '7ots servers are coming soon; meanwhile use your own server',
|
|
203
|
+
paidOff: '7ots servers are not enabled for your account yet; meanwhile use your own server',
|
|
204
|
+
paidUnavailable: '7ots servers are not available right now; try again later',
|
|
205
|
+
exists: 'This ot already has a server; delete or revoke it before creating another',
|
|
206
|
+
accountLimit: 'Your account already has {max} 7ots server(s) (its limit)',
|
|
207
|
+
full: 'No 7ots servers are available right now; try again later',
|
|
208
|
+
badSize: 'That server size is not available',
|
|
209
|
+
needsPayment: 'The 7ots server has not been paid for yet: click "Pay and create server" again',
|
|
210
|
+
userNeedsKey: 'To create the server in your Orquesta account, connect it with an oak_ key that has the projects:write and vms:write scopes (Orquesta → Settings → API keys). An OAuth connection cannot create servers.',
|
|
211
|
+
userScopes: 'Your Orquesta account did not allow creating the server: reconnect it with an oak_ key that has the projects:write and vms:write scopes (Orquesta → Settings → API keys)',
|
|
212
|
+
subscriptionInUse: 'Delete your 7ots servers first; then you can cancel the subscription',
|
|
191
213
|
},
|
|
192
214
|
},
|
|
193
215
|
};
|
|
@@ -260,6 +282,7 @@ const pt = {
|
|
|
260
282
|
busy: 'Já há uma tarefa em andamento ({id}); espere ela terminar',
|
|
261
283
|
noAgent: 'Esse projeto do Orquesta não tem agente: configure um agente no Orquesta ({url})',
|
|
262
284
|
noProject: 'Esse projeto do Orquesta não existe ou não está compartilhado com o 7ots',
|
|
285
|
+
notLinked: 'Este ot não está conectado a esse projeto no Orquesta, ou não pode executar tarefas lá. Conecte-o no projeto → Identidade → Ots conectados: {url}',
|
|
263
286
|
rateLimited: 'O Orquesta está recebendo muitas solicitações; espere um momento',
|
|
264
287
|
submitFailed: 'O Orquesta não aceitou a tarefa (erro {status})',
|
|
265
288
|
agentOffline: 'O agente deste projeto está desconectado: a tarefa fica na fila e começa quando ele voltar',
|
|
@@ -281,6 +304,16 @@ const pt = {
|
|
|
281
304
|
jobGone: 'Esse trabalho não existe mais ou já terminou',
|
|
282
305
|
llmLimit: 'Este trabalho já usou suas {max} chamadas ao modelo',
|
|
283
306
|
paidSoon: 'Os servidores do 7ots chegam em breve; enquanto isso use seu próprio servidor',
|
|
307
|
+
paidOff: 'Os servidores do 7ots ainda não estão ativados para sua conta; enquanto isso use seu próprio servidor',
|
|
308
|
+
paidUnavailable: 'Os servidores do 7ots não estão disponíveis agora; tente mais tarde',
|
|
309
|
+
exists: 'Este ot já tem um servidor; exclua ou revogue antes de criar outro',
|
|
310
|
+
accountLimit: 'Sua conta já tem {max} servidor(es) do 7ots (seu limite)',
|
|
311
|
+
full: 'Não há servidores do 7ots disponíveis agora; tente mais tarde',
|
|
312
|
+
badSize: 'Esse tamanho de servidor não está disponível',
|
|
313
|
+
needsPayment: 'O servidor da 7ots ainda não foi pago: clique de novo em «Pagar e criar servidor»',
|
|
314
|
+
userNeedsKey: 'Para criar o servidor na sua conta do Orquesta, conecte-a com uma chave oak_ que tenha as permissões projects:write e vms:write (Orquesta → Configurações → API keys). A conexão por OAuth não pode criar servidores.',
|
|
315
|
+
userScopes: 'Sua conta do Orquesta não permitiu criar o servidor: reconecte-a com uma chave oak_ que tenha as permissões projects:write e vms:write (Orquesta → Configurações → API keys)',
|
|
316
|
+
subscriptionInUse: 'Exclua antes seus servidores da 7ots; depois você pode cancelar a assinatura',
|
|
284
317
|
},
|
|
285
318
|
},
|
|
286
319
|
};
|
package/src/identity/random.js
CHANGED
|
@@ -33,13 +33,27 @@ const TEXT = {
|
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Semilla corta y legible (7 caracteres base36). Las de 7 caracteres sortean entre el catálogo
|
|
38
|
+
* completo de piezas; cualquier otra longitud (las de 6 de antes de 2026-10, las escritas a mano)
|
|
39
|
+
* usa solo el catálogo 1, así que sigue dando exactamente el mismo ot que siempre.
|
|
40
|
+
*/
|
|
37
41
|
export function newSeed() {
|
|
38
42
|
let s = '';
|
|
39
|
-
for (let i = 0; i <
|
|
43
|
+
for (let i = 0; i < 7; i++) s += Math.floor(Math.random() * 36).toString(36);
|
|
40
44
|
return s;
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
/** ¿La semilla usa el catálogo 1 (todas salvo las de 7 caracteres, ver newSeed)? */
|
|
48
|
+
export function isLegacySeed(seed) {
|
|
49
|
+
return cleanSeed(seed).length !== 7;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Personaje de una semilla (el mismo en la web, el CLI, la mascota y el visor 3D). */
|
|
53
|
+
export function seedCharacter(seed) {
|
|
54
|
+
return randomCharacter(seedNumber(seed), { legacy: isLegacySeed(seed) });
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
/** Normaliza una semilla escrita por una persona: minúsculas, [a-z0-9-], máx. 32. */
|
|
44
58
|
export function cleanSeed(seed) {
|
|
45
59
|
return String(seed ?? '').toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 32);
|
|
@@ -78,7 +92,7 @@ export function randomIdentity(seed, { lang = 'en', name } = {}) {
|
|
|
78
92
|
const L = TEXT[lang] ? lang : 'en';
|
|
79
93
|
const tx = TEXT[L];
|
|
80
94
|
const { rnd, pick } = rng(seedNumber(sd));
|
|
81
|
-
const character =
|
|
95
|
+
const character = seedCharacter(sd);
|
|
82
96
|
const nm = String(name || '').trim().slice(0, 40) || randomName(sd);
|
|
83
97
|
const traits = [];
|
|
84
98
|
while (traits.length < 3) {
|
package/src/identity/schema.js
CHANGED
|
@@ -66,6 +66,9 @@ export const IDENTITY_DEFAULTS = Object.freeze({
|
|
|
66
66
|
* @param {object} input
|
|
67
67
|
* @param {object} [base] identidad sobre la que se aplica (por defecto, los valores de serie)
|
|
68
68
|
*/
|
|
69
|
+
/** Campos de la ficha de personaje y su largo máximo. */
|
|
70
|
+
const SHEET = { backstory: 3000, style: 1500, quirks: 1500, scenario: 1500, greeting: 600, examples: 4000 };
|
|
71
|
+
|
|
69
72
|
export function defineIdentity(input = {}, base = IDENTITY_DEFAULTS) {
|
|
70
73
|
const i = input && typeof input === 'object' ? input : {};
|
|
71
74
|
const b = base;
|
|
@@ -87,6 +90,8 @@ export function defineIdentity(input = {}, base = IDENTITY_DEFAULTS) {
|
|
|
87
90
|
tone: str(personality.tone, 120),
|
|
88
91
|
traits: list(personality.traits, 12).map((t) => str(t, 40)).filter(Boolean),
|
|
89
92
|
instructions: str(personality.instructions, 4000),
|
|
93
|
+
// ficha de personaje (la misma del pet de escritorio; privada como las instrucciones)
|
|
94
|
+
sheet: Object.fromEntries(Object.entries(SHEET).map(([k, n]) => [k, str(personality.sheet?.[k], n)]).filter(([, v]) => v)),
|
|
90
95
|
},
|
|
91
96
|
look: {
|
|
92
97
|
kind: LOOK_KINDS.includes(look.kind) ? look.kind : look.image ? 'image' : 'character',
|
|
@@ -96,6 +101,7 @@ export function defineIdentity(input = {}, base = IDENTITY_DEFAULTS) {
|
|
|
96
101
|
accent: color(look.accent) || IDENTITY_DEFAULTS.look.accent,
|
|
97
102
|
emoji: str(look.emoji, 8),
|
|
98
103
|
image: url(look.image),
|
|
104
|
+
card: { foil: ['holo', 'prism', 'cosmos', 'gold', 'none'].includes(look.card?.foil) ? look.card.foil : '' },
|
|
99
105
|
avatar: {
|
|
100
106
|
url: url(look.avatar?.url),
|
|
101
107
|
body: look.avatar?.body === 'M' ? 'M' : 'F',
|
|
@@ -195,6 +201,10 @@ export function identityPrompt(id, locale = id.language) {
|
|
|
195
201
|
id.bio ? t('identity.prompt.about', { bio: id.bio }) : '',
|
|
196
202
|
p.tone ? t('identity.prompt.tone', { tone: p.tone }) : '',
|
|
197
203
|
p.traits.length ? t('identity.prompt.traits', { traits: p.traits.join(', ') }) : '',
|
|
204
|
+
p.sheet?.backstory,
|
|
205
|
+
p.sheet?.style,
|
|
206
|
+
p.sheet?.quirks,
|
|
207
|
+
p.sheet?.examples,
|
|
198
208
|
p.instructions,
|
|
199
209
|
]
|
|
200
210
|
.filter(Boolean)
|