feat: integrate room framework and isolated subgame bundles
Complete room UI and protocol integration, move game definitions and resources behind bundle entries, publish authoritative version XML, and document single-game builds. Include all current resource changes and experiment artifacts.
This commit is contained in:
+99
-26
@@ -107,13 +107,14 @@ function round(value) {
|
||||
|
||||
function sourceSignature(spirit) {
|
||||
const isFullScreen = spirit.width === 1280 && spirit.height === 720;
|
||||
const autoSizeText = spirit.text !== undefined && spirit.autoSize === 1;
|
||||
return JSON.stringify([
|
||||
spirit.name,
|
||||
round(spirit.x + spirit.width / 2 - 640),
|
||||
round(360 - (spirit.y + spirit.height / 2)),
|
||||
isFullScreen ? 1600 : spirit.width,
|
||||
isFullScreen ? 720 : spirit.height,
|
||||
spirit.visible,
|
||||
autoSizeText ? null : isFullScreen ? 1600 : spirit.width,
|
||||
autoSizeText ? null : isFullScreen ? 720 : spirit.height,
|
||||
[3112,3114,3117].includes(spirit.id) ? false : spirit.visible,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -122,18 +123,28 @@ function prefabSignature(prefab, node) {
|
||||
.map((ref) => prefab[ref.__id__])
|
||||
.find((component) => component.__type__ === 'cc.UITransform');
|
||||
assert.ok(uiTransformRef, `${node._name} should have UITransform`);
|
||||
const autoSizeText = nodeComponent(prefab,node,'cc.Label')?._overflow === 0;
|
||||
|
||||
return JSON.stringify([
|
||||
node._name,
|
||||
round(node._lpos.x),
|
||||
round(node._lpos.y),
|
||||
uiTransformRef._contentSize.width,
|
||||
uiTransformRef._contentSize.height,
|
||||
autoSizeText ? null : uiTransformRef._contentSize.width,
|
||||
autoSizeText ? null : uiTransformRef._contentSize.height,
|
||||
node._active,
|
||||
]);
|
||||
}
|
||||
|
||||
test('Layer418 prefab preserves every source spirit and its layout contract', () => {
|
||||
// Approved replacements: EditBox input; dynamic phrase rows; expression grid;
|
||||
// history row template; scrollable announcements and their dynamic cost label.
|
||||
// All other source art stays. Spirit3117 retains the user's authored font size 16.
|
||||
const dynamicSourceIds = new Set([
|
||||
204, 206, 207, 208, 209, 210, 211, 212,
|
||||
...Array.from({length:20},(_,index)=>528+index),
|
||||
467, 696, 697, 163, 164, 165, 480, 3109, 3110, 3117,
|
||||
]);
|
||||
|
||||
test('Layer418 static source art preserves its layout and sprite contract', () => {
|
||||
const sourceSpirits = parseSourceSpirits(fs.readFileSync(sourcePath, 'utf8'));
|
||||
const objectList = JSON.parse(fs.readFileSync(objectDataPath, 'utf8')).ObjectList;
|
||||
const imageFileList = JSON.parse(fs.readFileSync(imageDataPath, 'utf8')).ImageFileList;
|
||||
@@ -141,31 +152,21 @@ test('Layer418 prefab preserves every source spirit and its layout contract', ()
|
||||
const nodes = prefab.filter((entry) => entry.__type__ === 'cc.Node');
|
||||
const root = nodes.find((node) => node._name === 'Layer418_ChatPannel');
|
||||
const group = nodes.find((node) => node._name === 'group-30');
|
||||
const spiritNodes = nodes.filter(
|
||||
(node) => node._name !== 'Layer418_ChatPannel' && node._name !== 'group-30',
|
||||
);
|
||||
|
||||
assert.equal(sourceSpirits.length, 54);
|
||||
assert.equal(nodes.length, 56);
|
||||
assert.equal(spiritNodes.length, 54);
|
||||
const staticSpirits = sourceSpirits.filter(spirit=>!dynamicSourceIds.has(spirit.id));
|
||||
const staticNames = new Set(staticSpirits.map(spirit=>spirit.name));
|
||||
const spiritNodes = nodes.filter(node=>staticNames.has(node._name));
|
||||
assert.equal(spiritNodes.length,staticSpirits.length);
|
||||
assert.deepEqual(root._children.map((ref) => prefab[ref.__id__]._name), ['group-30']);
|
||||
assert.deepEqual(
|
||||
group._children.map((ref) => prefab[ref.__id__]._name),
|
||||
[...sourceSpirits].sort((a, b) => a.index - b.index).map((spirit) => spirit.name),
|
||||
group._children.map((ref) => prefab[ref.__id__]._name).filter(name=>staticNames.has(name)),
|
||||
[...staticSpirits].sort((a, b) => a.index - b.index).map((spirit) => spirit.name),
|
||||
);
|
||||
assert.deepEqual(
|
||||
spiritNodes.map((node) => prefabSignature(prefab, node)).sort(),
|
||||
sourceSpirits.map(sourceSignature).sort(),
|
||||
staticSpirits.map(sourceSignature).sort(),
|
||||
);
|
||||
|
||||
const types = prefab.reduce((counts, entry) => {
|
||||
counts[entry.__type__] = (counts[entry.__type__] ?? 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
assert.equal(types['cc.Sprite'], 48);
|
||||
assert.equal(types['cc.Label'], 6);
|
||||
|
||||
for (const spirit of sourceSpirits) {
|
||||
for (const spirit of staticSpirits) {
|
||||
const sourceObject = objectList[spirit.id]?.Property;
|
||||
assert.equal(sourceObject?.ObjectID, spirit.id, `ObjectList[${spirit.id}] must be direct indexed`);
|
||||
assert.equal(sourceObject.ObjectName, spirit.name);
|
||||
@@ -196,9 +197,9 @@ test('Layer418 labels preserve text, color, wrapping, and AutoSize semantics', (
|
||||
const objectList = JSON.parse(fs.readFileSync(objectDataPath, 'utf8')).ObjectList;
|
||||
const prefab = JSON.parse(fs.readFileSync(prefabPath, 'utf8'));
|
||||
const nodes = prefab.filter((entry) => entry.__type__ === 'cc.Node');
|
||||
const labels = sourceSpirits.filter((spirit) => objectList[spirit.id]?.Property.ObjectType === 4);
|
||||
const labels = sourceSpirits.filter((spirit) => !dynamicSourceIds.has(spirit.id)&&objectList[spirit.id]?.Property.ObjectType === 4);
|
||||
|
||||
assert.equal(labels.length, 6);
|
||||
assert.ok(labels.length>0);
|
||||
for (const spirit of labels) {
|
||||
const sourceObject = objectList[spirit.id].Property;
|
||||
const node = nodes.find((candidate) => candidate._name === spirit.name);
|
||||
@@ -231,3 +232,75 @@ test('Layer418 sliced sprite frames preserve their source border contract', () =
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Layer418 dynamic panels own clipped scroll content, layouts and reusable rows',()=>{
|
||||
const prefab=JSON.parse(fs.readFileSync(prefabPath,'utf8'));
|
||||
const nodes=prefab.filter(entry=>entry.__type__==='cc.Node');
|
||||
const group=nodes.find(node=>node._name==='group-30');
|
||||
const child=(node,name)=>{
|
||||
const matches=node._children.map(ref=>prefab[ref.__id__]).filter(item=>item._name===name);
|
||||
assert.equal(matches.length,1,`${node._name}/${name} must be unique`);
|
||||
return matches[0];
|
||||
};
|
||||
const input=child(group,'聊天输入框'),edit=nodeComponent(prefab,input,'cc.EditBox');
|
||||
assert.ok(edit,'input must be a real EditBox');
|
||||
for(const field of ['_textLabel','_placeholderLabel']){
|
||||
const label=prefab[edit[field]?.__id__];
|
||||
assert.equal(label?.__type__,'cc.Label',`${field} must resolve to a Label`);
|
||||
assert.equal(prefab[prefab[label.node.__id__]._parent.__id__],input);
|
||||
}
|
||||
const cost=child(group,'Spirit3117'),costLabel=nodeComponent(prefab,cost,'cc.Label');
|
||||
assert.equal(cost._active,false,'announcement cost is hidden until policy selection');
|
||||
assert.equal(costLabel._fontSize,16,'preserve authored cost-label font size');
|
||||
assert.equal(costLabel._overflow,1,'cost uses a bounded line');
|
||||
assert.equal(costLabel._string,'','cost is populated from remote policy');
|
||||
assert.equal(nodeComponent(prefab,cost,'cc.UITransform')._contentSize.width,520);
|
||||
for(const name of ['Phrases','History','Expressions','Announcements']){
|
||||
const panel=child(group,name),viewport=child(panel,'Viewport'),content=child(viewport,'Content');
|
||||
assert.equal(panel._active,name==='Phrases',`${name} initial tab state`);
|
||||
const scroll=nodeComponent(prefab,panel,'cc.ScrollView');
|
||||
assert.ok(scroll,`${name} ScrollView`);
|
||||
assert.equal(prefab[scroll._content.__id__],content,`${name} content reference`);
|
||||
assert.equal(scroll.horizontal,false);assert.equal(scroll.vertical,true);
|
||||
assert.ok(nodeComponent(prefab,viewport,'cc.Mask'),`${name} clipping mask`);
|
||||
const layout=nodeComponent(prefab,content,'cc.Layout');
|
||||
assert.ok(layout,`${name} Layout`);
|
||||
assert.equal(layout._resizeMode,1,`${name} grows content container`);
|
||||
assert.equal(layout._layoutType,name==='Expressions'?3:2,`${name} layout mode`);
|
||||
assert.equal(nodeComponent(prefab,content,'cc.UITransform')._anchorPoint.y,1,`${name} content top anchor`);
|
||||
if(name==='Phrases'){
|
||||
const row=child(content,'ChatPhraseItem');
|
||||
assert.ok(nodeComponent(prefab,child(row,'Text'),'cc.Label'));
|
||||
assert.ok(nodeComponent(prefab,child(row,'Background'),'cc.Sprite')._spriteFrame?.__uuid__);
|
||||
}else if(name==='History'){
|
||||
const row=child(content,'ChatHistoryItem');
|
||||
for(const label of ['Spirit164','Spirit3110','Spirit480'])assert.ok(nodeComponent(prefab,child(row,label),'cc.Label'));
|
||||
for(const sprite of ['Spirit163','Spirit165','Spirit697'])assert.ok(nodeComponent(prefab,child(row,sprite),'cc.Sprite')._spriteFrame?.__uuid__);
|
||||
}else if(name==='Expressions'){
|
||||
assert.equal(content._children.length,20);
|
||||
for(let index=1;index<=20;index++)assert.ok(nodeComponent(prefab,child(content,`Expression${index}`),'cc.Sprite')._spriteFrame?.__uuid__);
|
||||
}else assert.equal(content._children.length,0,'announcements are populated from state');
|
||||
}
|
||||
});
|
||||
|
||||
test('Layer418 serialized node, component and prefab references remain intact',()=>{
|
||||
const prefab=JSON.parse(fs.readFileSync(prefabPath,'utf8'));
|
||||
const visit=value=>{
|
||||
if(!value||typeof value!=='object')return;
|
||||
if(Object.prototype.hasOwnProperty.call(value,'__id__')){
|
||||
assert.ok(Number.isInteger(value.__id__)&&value.__id__>=0&&value.__id__<prefab.length,`dangling reference ${value.__id__}`);
|
||||
}
|
||||
Object.values(value).forEach(visit);
|
||||
};
|
||||
prefab.forEach(visit);
|
||||
for(const [index,node] of prefab.entries()){
|
||||
if(node.__type__!=='cc.Node')continue;
|
||||
assert.equal(prefab[node._prefab?.__id__]?.__type__,'cc.PrefabInfo',`${node._name} prefab info`);
|
||||
for(const ref of node._children)assert.equal(prefab[ref.__id__]._parent.__id__,index,`${node._name} child parent backlink`);
|
||||
for(const ref of node._components){
|
||||
const component=prefab[ref.__id__];
|
||||
assert.equal(component.node.__id__,index,`${node._name} component backlink`);
|
||||
assert.equal(prefab[component.__prefab?.__id__]?.__type__,'cc.CompPrefabInfo',`${node._name} component prefab info`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user