联机功能开发

This commit is contained in:
daixiawu 2026-05-27 22:46:45 +08:00
parent d1c226e717
commit a5a0bf51d2
44 changed files with 10699 additions and 8680 deletions

View File

@ -1,5 +1,5 @@
{
"nextId": 270,
"nextId": 272,
"bugs": [
{
"id": 2,
@ -2672,6 +2672,28 @@
"longTerm": false,
"createdAt": 1779810291469,
"updatedAt": 1779810291469
},
{
"id": 270,
"title": "战地协同 标记不一样",
"description": "",
"status": "open",
"priority": "medium",
"module": "",
"longTerm": false,
"createdAt": 1779882909992,
"updatedAt": 1779882909992
},
{
"id": 271,
"title": "推测为占领全部城市后间谍单位消失但没有及时更新状态",
"description": "",
"status": "open",
"priority": "medium",
"module": "",
"longTerm": false,
"createdAt": 1779891352529,
"updatedAt": 1779891352529
}
]
}

File diff suppressed because one or more lines are too long

View File

@ -39,6 +39,11 @@ MonoBehaviour:
OutsideMultiplayOpenHint: "\u4ECD\u6709\u5F85\u52A0\u5165\u7684\u5E2D\u4F4D\uFF0C\u8BF7\u51CF\u5C11\u5E2D\u4F4D"
OutsideMultiplayRoomFull: "\u623F\u95F4\u5DF2\u6EE1"
OutsideMultiplayRoomGone: "\u623F\u95F4\u5DF2\u89E3\u6563\u6216\u5DF2\u5F00\u59CB\u6E38\u620F"
OutsideMultiplayRoomNameIllegal: "\u623F\u95F4\u540D\u79F0\u53EF\u80FD\u5B58\u5728\u4E0D\u5408\u9002\u7528\u8BCD\uFF0C\u8BF7\u91CD\u65B0\u8BBE\u7F6E"
OutsideMultiplayRoomNameTooShort: "\u623F\u95F4\u540D\u79F0\u8FC7\u77ED"
OutsideMultiplayRoomNetError: "\u7F51\u7EDC\u9519\u8BEF"
OutsideMultiplayRoomJubaoSuccess: "\u4E3E\u62A5\u5DF2\u63D0\u4EA4"
OutsideMultiplayRoomMuteSuccess: "\u5DF2\u9690\u85CF\u8BE5\u623F\u95F4\uFF0C24\u5C0F\u65F6\u5185\u4E0D\u518D\u663E\u793A"
OutsideHistoryDropListNoLimitP: "\u4E0D\u9650"
OutsideHistoryDropList2P: "2\u4EBA"
OutsideHistoryDropList3P: "3\u4EBA"

View File

@ -798,3 +798,4 @@ MonoBehaviour:
Priority: 0
EnableAction: 0
EnableActions: []
CultureTaskDataList: []

File diff suppressed because one or more lines are too long

View File

@ -39,6 +39,11 @@ MonoBehaviour:
OutsideMultiplayOpenHint: 20077
OutsideMultiplayRoomFull: 20078
OutsideMultiplayRoomGone: 20079
OutsideMultiplayRoomNameIllegal: 20093
OutsideMultiplayRoomNameTooShort: 20094
OutsideMultiplayRoomNetError: 17451
OutsideMultiplayRoomJubaoSuccess: 20095
OutsideMultiplayRoomMuteSuccess: 20096
OutsideHistoryDropListNoLimitP: 17352
OutsideHistoryDropList2P: 17353
OutsideHistoryDropList3P: 17354
@ -50,6 +55,9 @@ MonoBehaviour:
OutsideHistoryDropListTimeOrder: 17360
OutsideHistoryDropListTimeOrderR: 17361
OutsideHistoryDropListScoreOrder: 17362
BugReportUploadingHint:
BugReportUploadingSuccessHint:
BugReportUploadingFailedHint:
HeroTaskFinishedDesc: 2360
GridInfoText_PerTurn: 2503
GridInfoText_Population: 2504

View File

@ -387,7 +387,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 16949
ID: 20080
FontID: 2
TextCfg:
- Type: 1

View File

@ -162,6 +162,41 @@ MonoBehaviour:
m_OnValueChanged:
m_PersistentCalls:
m_Calls: []
--- !u!1 &2078440796911255766
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2559237646761523103}
m_Layer: 5
m_Name: NetErrorPanelRoot
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2559237646761523103
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2078440796911255766}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7448979660496629192}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -671.3968, y: 408.8651}
m_SizeDelta: {x: 577.2065, y: 97.9193}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &2440790000169401958
GameObject:
m_ObjectHideFlags: 0
@ -197,6 +232,7 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4797701493746855371}
- {fileID: 2559237646761523103}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}

View File

@ -109,11 +109,11 @@ RectTransform:
- {fileID: 318087679502976148}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -110.46619, y: -409.99628}
m_SizeDelta: {x: 692.1076, y: 165.3526}
m_Pivot: {x: 0.5, y: 0.5}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 691.1385, y: 68.448}
m_Pivot: {x: 0, y: 1}
--- !u!114 &-1084591119027101579
MonoBehaviour:
m_ObjectHideFlags: 0
@ -324,7 +324,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 389.04, y: -129.46}
m_AnchoredPosition: {x: 389.04, y: -42.460007}
m_SizeDelta: {x: 606.13, y: 71.777}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &2833781254162354808
@ -835,7 +835,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 389.04, y: -93.572}
m_AnchoredPosition: {x: 389.04, y: -6.5719986}
m_SizeDelta: {x: 606.13, y: 0}
m_Pivot: {x: 0.5, y: 0}
--- !u!114 &4393837735354183377
@ -1085,7 +1085,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 47, y: -123}
m_AnchoredPosition: {x: 47, y: -36}
m_SizeDelta: {x: 47.909485, y: 47.9095}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1808859806862558063

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 425dde724c862994887949c605a5e0ee
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -336,6 +336,8 @@ MonoBehaviour:
PasswordInput: {fileID: 5655071534055511772}
PasswordHintObject: {fileID: 5872298964086764219}
PasswordHintText: {fileID: 8047929761724619342}
RoomNameHintObject: {fileID: 9107250659884233687}
RoomNameHintText: {fileID: 8432045260240010102}
--- !u!1 &1712044533332343156
GameObject:
m_ObjectHideFlags: 0
@ -703,6 +705,160 @@ MonoBehaviour:
ID: 20063
FontID: 0
TextCfg: []
--- !u!1 &2383265354460307738
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 9196843804145772340}
- component: {fileID: 4147712615985337980}
- component: {fileID: 7197738449294962291}
- component: {fileID: 4916706260937510150}
m_Layer: 5
m_Name: RoomNameLegalHint
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &9196843804145772340
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2383265354460307738}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7463303194629128016}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 459.67932, y: 46.5352}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4147712615985337980
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2383265354460307738}
m_CullTransparentMesh: 1
--- !u!114 &7197738449294962291
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2383265354460307738}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: "\u8BF7\u4F7F\u7528\u53CB\u5584\u3001\u6E05\u6670\u3001\u9002\u5408\u516C\u5F00\u5C55\u793A\u7684\u623F\u95F4\u540D\u79F0\uFF0C\u5171\u540C\u521B\u5EFA\u5065\u5EB7\u6587\u660E\u7684\u8054\u673A\u73AF\u5883\u3002"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8e119f168f1a6b745be02ef19f51610f, type: 2}
m_sharedMaterial: {fileID: -8081454072124122709, guid: 8e119f168f1a6b745be02ef19f51610f, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4287401100
m_fontColor: {r: 0.5471698, g: 0.5471698, b: 0.5471698, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 18.6
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 14
m_fontSizeMax: 22
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!114 &4916706260937510150
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2383265354460307738}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6b27f832d22e4a8d916272b644937774, type: 3}
m_Name:
m_EditorClassIdentifier:
Ban: 0
NoExport: 0
FontBan: 0
Preset: 0
ID: 20085
FontID: 0
TextCfg: []
--- !u!1 &2960065425293172538
GameObject:
m_ObjectHideFlags: 0
@ -2984,10 +3140,11 @@ RectTransform:
- {fileID: 2823495548145270491}
- {fileID: 7559943356464538090}
- {fileID: 4104221981773057835}
- {fileID: 5055858450282614166}
- {fileID: 9196843804145772340}
- {fileID: 287912720796096540}
- {fileID: 5266944002495899053}
- {fileID: 748938986039668253}
- {fileID: 5055858450282614166}
m_Father: {fileID: 1937094061063432054}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
@ -5099,12 +5256,12 @@ GameObject:
- component: {fileID: 8432045260240010102}
- component: {fileID: -3369302157754019626}
m_Layer: 5
m_Name: NoRoomNameHint
m_Name: RoomNameHint
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!224 &5055858450282614166
RectTransform:
m_ObjectHideFlags: 0

View File

@ -20,7 +20,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &1937094061063432054
RectTransform:
m_ObjectHideFlags: 0
@ -1249,7 +1249,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 20064
ID: 20088
FontID: 0
TextCfg: []
--- !u!1 &5921627962211736030
@ -1403,7 +1403,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 2233
ID: 20087
FontID: 2
TextCfg:
- Type: 1
@ -1697,7 +1697,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 2228
ID: 20086
FontID: 2
TextCfg:
- Type: 1
@ -2037,7 +2037,7 @@ MonoBehaviour:
m_CharacterValidation: 0
m_RegexValue:
m_GlobalPointSize: 14
m_CharacterLimit: 0
m_CharacterLimit: 4
m_OnEndEdit:
m_PersistentCalls:
m_Calls: []

View File

@ -1136,6 +1136,81 @@ MonoBehaviour:
LineSpacing: 0
ApplyParagraphSpacing: 0
ParagraphSpacing: 0
--- !u!1 &5759544334706913819
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4652863007342779236}
- component: {fileID: 5410305486104802002}
- component: {fileID: 4825784848556651340}
m_Layer: 5
m_Name: Lock
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4652863007342779236
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5759544334706913819}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2527218108953289808}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 359, y: 1}
m_SizeDelta: {x: 42.3955, y: 51.408}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5410305486104802002
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5759544334706913819}
m_CullTransparentMesh: 1
--- !u!114 &4825784848556651340
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5759544334706913819}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.509434, g: 0.509434, b: 0.509434, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: c4149874829f08d478da4bfae558335a, type: 3}
m_Type: 0
m_PreserveAspect: 1
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &5763353435607814537
GameObject:
m_ObjectHideFlags: 0
@ -1287,7 +1362,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 17377
ID: 20089
FontID: 2
TextCfg:
- Type: 1
@ -1501,6 +1576,7 @@ RectTransform:
- {fileID: 2522315637815645420}
- {fileID: 6452178927417364351}
- {fileID: 1809688155015995307}
- {fileID: 4652863007342779236}
- {fileID: 555533381074390939}
- {fileID: 96625707259803793}
- {fileID: 768525217504304060}
@ -1569,6 +1645,7 @@ MonoBehaviour:
VersionText: {fileID: 0}
JoinButton: {fileID: 5777197804309038576}
ActionButton: {fileID: 5800461245087449632}
Lock: {fileID: 5759544334706913819}
--- !u!1 &8852519433211468652
GameObject:
m_ObjectHideFlags: 0
@ -1581,8 +1658,6 @@ GameObject:
- component: {fileID: 5815633150663516744}
- component: {fileID: 60679182699258626}
- component: {fileID: 5800461245087449632}
- component: {fileID: 1345292219526059393}
- component: {fileID: 8403910360848460228}
m_Layer: 5
m_Name: JubaoButton
m_TagString: Untagged
@ -1692,116 +1767,3 @@ MonoBehaviour:
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!82 &1345292219526059393
AudioSource:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8852519433211468652}
m_Enabled: 1
serializedVersion: 4
OutputAudioMixerGroup: {fileID: 0}
m_audioClip: {fileID: 0}
m_PlayOnAwake: 1
m_Volume: 1
m_Pitch: 1
Loop: 0
Mute: 0
Spatialize: 0
SpatializePostEffects: 0
Priority: 128
DopplerLevel: 1
MinDistance: 1
MaxDistance: 500
Pan2D: 0
rolloffMode: 0
BypassEffects: 0
BypassListenerEffects: 0
BypassReverbZones: 0
rolloffCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
panLevelCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
spreadCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
reverbZoneMixCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
--- !u!114 &8403910360848460228
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8852519433211468652}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 33d75335a9dad784a91baba5578371fb, type: 3}
m_Name:
m_EditorClassIdentifier:
hoverSound: {fileID: 0}
targetScale: 1.1
clickSound: {fileID: 0}
pressScale: 0.8
scaleDuration: 0.1

View File

@ -151,7 +151,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 19879
ID: 20092
FontID: 0
TextCfg: []
--- !u!1 &1484864756966099653
@ -668,7 +668,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 956.9043, y: -532}
m_SizeDelta: {x: 0, y: 322.3732}
m_SizeDelta: {x: 0, y: 347.869}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &9162338914215527152
CanvasRenderer:
@ -1031,7 +1031,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 99
ID: 20089
FontID: 2
TextCfg:
- Type: 1
@ -1620,7 +1620,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 17706
ID: 20090
FontID: 2
TextCfg:
- Type: 1
@ -1975,8 +1975,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 475.04083, y: -253.69449}
m_SizeDelta: {x: 830.08167, y: 137.3574}
m_AnchoredPosition: {x: 475.04083, y: -279.19028}
m_SizeDelta: {x: 0, y: 137.3574}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1640998311177812195
MonoBehaviour:
@ -2085,7 +2085,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: "\u5C4F\u853D\u623F\u95F4\u540E\u5C06\u57281\u5929\u5185\u4E0D\u518D\u663E\u793A\u8BE5\u73A9\u5BB6\u7684\u623F\u95F4\uFF0C\u4E3E\u62A5\u529F\u80FD\u4ECD\u5728\u5F00\u53D1\u4E2D\u3002"
m_text: "\u5C4F\u853D\u623F\u95F4\u540E\u5C06\u572824\u5C0F\u65F6\u5185\u4E0D\u518D\u663E\u793A\u8BE5\u623F\u95F4\u3002\u88AB\u591A\u4EBA\u4E3E\u62A5\u7684\u623F\u95F4\u5C06\u4F1A\u81EA\u52A8\u8C03\u6574\u623F\u95F4\u540D\u79F0\u4E3A\u9ED8\u8BA4\u540D\u3002"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8e119f168f1a6b745be02ef19f51610f, type: 2}
m_sharedMaterial: {fileID: -8081454072124122709, guid: 8e119f168f1a6b745be02ef19f51610f, type: 2}
@ -2170,7 +2170,7 @@ MonoBehaviour:
NoExport: 0
FontBan: 0
Preset: 0
ID: 17707
ID: 20101
FontID: 1
TextCfg:
- Type: 1

View File

@ -135,7 +135,10 @@ namespace TH1_Core.Events
ExamineCityExp,
InfiltrateStealCoin,
OutsideMultiplayRoomFull,
OutsideMultiplayRoomGone
OutsideMultiplayRoomGone,
OutsideMultiplayRoomNetError,
OutsideMultiplayOpenHint,
OutsideMultiplayCantStartCount
}
public struct ShowUINotifyCommon
{

View File

@ -113,6 +113,7 @@ namespace RuntimeData
[MemoryPackOnDeserialized]
public void OnAfterMemoryPackDeserialize()
{
MatchSettlement = MatchSettlementInfo.NormalizeType(MatchSettlement);
if (TimeLimitSeconds == 0) TimeLimitSeconds = 180;
// 旧存档兼容WaterType 字段不存在时默认为 Pangea
if (!System.Enum.IsDefined(typeof(Logic.MapWaterType), WaterType))
@ -1952,6 +1953,8 @@ namespace RuntimeData
Net ??= new NetData();
Net.Players ??= new Dictionary<ulong, uint>();
Net.Actions ??= new List<ActionNetData>();
if (MapConfig != null) MapConfig.MatchSettlement = MatchSettlementInfo.NormalizeType(MapConfig.MatchSettlement);
if (MatchSettlement != null) MatchSettlement.SettlementType = MatchSettlementInfo.NormalizeType(MatchSettlement.SettlementType);
CityToPlayerDict ??= new Dictionary<uint, uint>();
UnitToCityDict ??= new Dictionary<uint, uint>();
UnitToGridDict ??= new Dictionary<uint, uint>();

View File

@ -149,6 +149,33 @@ public class CultureCardInfo
}
}
[Serializable]
public class CultureTaskInfo
{
[Tooltip("文化任务标识,对应 MomentItemBase.GetMomentSubType()")]
public MomentSubType TaskType;
[Tooltip("是否启用该文化任务文案")]
public bool IsActive = true;
[MultilingualField]
[Tooltip("文化任务名称")]
public string Name;
[MultilingualField]
[Tooltip("文化任务描述")]
public string Description;
[Tooltip("文化任务图标")]
public Sprite Icon;
[Tooltip("文化任务奖励文化值,仅用于展示,实际奖励仍由 MomentItemBase 控制")]
public int CultureValue = 5;
[Tooltip("排序优先级,数值越小越靠前")]
public int Priority = 0;
}
[CreateAssetMenu(fileName = "CultureCardDataAssets", menuName = "TH1 Game Data/CultureCard Data Asset")]
public class CultureCardDataAssets : ScriptableObject
@ -156,10 +183,15 @@ public class CultureCardDataAssets : ScriptableObject
[Header("市政卡数据列表")]
[Tooltip("所有市政卡的基础配置数据")]
public List<CultureCardInfo> CultureCardDataList = new List<CultureCardInfo>();
[Header("文化任务数据列表")]
[Tooltip("文化任务的多语言文案与展示数据")]
public List<CultureTaskInfo> CultureTaskDataList = new List<CultureTaskInfo>();
// 运行时缓存字典
[NonSerialized] private Dictionary<CultureCardType, CultureCardInfo> _cardInfoDict;
[NonSerialized] private Dictionary<MomentSubType, CultureTaskInfo> _cultureTaskInfoDict;
[NonSerialized] private bool _initialized = false;
@ -168,7 +200,7 @@ public class CultureCardDataAssets : ScriptableObject
/// </summary>
public void Init()
{
if (_initialized && _cardInfoDict != null) return;
if (_initialized && _cardInfoDict != null && _cultureTaskInfoDict != null) return;
_cardInfoDict = new Dictionary<CultureCardType, CultureCardInfo>();
foreach (var cardInfo in CultureCardDataList)
@ -182,6 +214,18 @@ public class CultureCardDataAssets : ScriptableObject
_cardInfoDict[cardInfo.CardType] = cardInfo;
}
_cultureTaskInfoDict = new Dictionary<MomentSubType, CultureTaskInfo>();
foreach (var taskInfo in CultureTaskDataList)
{
if (taskInfo.TaskType == MomentSubType.None) continue;
if (_cultureTaskInfoDict.ContainsKey(taskInfo.TaskType))
{
Debug.LogWarning($"[CultureCardDataAssets] 重复的文化任务类型: {taskInfo.TaskType}");
continue;
}
_cultureTaskInfoDict[taskInfo.TaskType] = taskInfo;
}
_initialized = true;
}
@ -197,4 +241,10 @@ public class CultureCardDataAssets : ScriptableObject
Init();
return _cardInfoDict.TryGetValue(cardType, out cardInfo) && cardInfo.IsActive;
}
public bool GetCultureTaskInfo(MomentSubType taskType, out CultureTaskInfo taskInfo)
{
Init();
return _cultureTaskInfoDict.TryGetValue(taskType, out taskInfo) && taskInfo.IsActive;
}
}

View File

@ -87,6 +87,13 @@ public enum MomentSubType
ExploitFirstBigMarket = 402009,
ExploitFirstBigForge = 402010,
ExploitFirstBigSawmill = 402011,
ExploitWonderPEACE = 402012,
ExploitWonderKNOWLEDGE = 402013,
ExploitWonderTRADE = 402014,
ExploitWonderWEALTH = 402015,
ExploitWonderPOWER = 402016,
ExploitWonderPARK = 402017,
ExploitWonderEYE = 402018,
}
[Serializable]
@ -206,4 +213,4 @@ public struct MomentImagePack
public Sprite Image;
public Empire Empire;
}
}

View File

@ -43,6 +43,11 @@ public class TextDataAssets : ScriptableObject
[MultilingualField]public string OutsideMultiplayOpenHint;
[MultilingualField]public string OutsideMultiplayRoomFull;
[MultilingualField]public string OutsideMultiplayRoomGone;
[MultilingualField]public string OutsideMultiplayRoomNameIllegal;
[MultilingualField]public string OutsideMultiplayRoomNameTooShort;
[MultilingualField]public string OutsideMultiplayRoomNetError;
[MultilingualField]public string OutsideMultiplayRoomJubaoSuccess;
[MultilingualField]public string OutsideMultiplayRoomMuteSuccess;
[MultilingualField]public string OutsideHistoryDropListNoLimitP;
[MultilingualField]public string OutsideHistoryDropList2P;
@ -56,6 +61,10 @@ public class TextDataAssets : ScriptableObject
[MultilingualField]public string OutsideHistoryDropListTimeOrderR;
[MultilingualField]public string OutsideHistoryDropListScoreOrder;
[MultilingualField]public string BugReportUploadingHint;
[MultilingualField]public string BugReportUploadingSuccessHint;
[MultilingualField]public string BugReportUploadingFailedHint;
//-------- UI --------
[MultilingualField] public string HeroTaskFinishedDesc;

View File

@ -1328,16 +1328,6 @@ namespace Logic.Action
//Step #3 更新成就完成情况
AchievementDataManager.Instance.OnBuildWonder(actionParams.MapData, player, city, actionParams.GridData);
//Step #4 Moment
if (actionParams.MapData == Main.MapData &&
actionParams.PlayerData == Main.MapData.PlayerMap.SelfPlayerData)
{
EventManager.Publish(new ShowUINotifyMoment()
{
MomentSubType = MomentSubType.ExploitWonder,
Empire = Main.MapData.PlayerMap.SelfPlayerData.Empire
});
}
return true;
}

View File

@ -10,19 +10,19 @@ namespace Logic.Editor
{
public static class DumpMapConfigEditor
{
[MenuItem("Tools/Debug/Dump 关卡2配置(Domination)")]
[MenuItem("Tools/Debug/Dump 关卡2配置(Normal)")]
private static void DumpId2()
{
DumpId(2);
}
[MenuItem("Tools/Debug/Dump 关卡6配置(Perfect)")]
[MenuItem("Tools/Debug/Dump 关卡6配置(Normal)")]
private static void DumpId6()
{
DumpId(6);
}
[MenuItem("Tools/Debug/Dump 关卡7配置(Creative)")]
[MenuItem("Tools/Debug/Dump 关卡7配置(Normal)")]
private static void DumpId7()
{
DumpId(7);

View File

@ -20,11 +20,9 @@ namespace TH1_Logic.MatchConfig
public enum MatchSettlementType
{
None = 0,
Domination = 1,
Perfect = 2,
Normal = 1,
Tutor = 3,
Story = 4,
Creative = 5,
}
@ -34,6 +32,19 @@ namespace TH1_Logic.MatchConfig
public MatchSettlementType SettlementType;
public bool IsFinished;
public Dictionary<uint, PlayerSettlementGroup> PlayerSettlements;
public static MatchSettlementType NormalizeType(MatchSettlementType matchType)
{
return (int)matchType switch
{
1 => MatchSettlementType.Normal,
2 => MatchSettlementType.Normal,
5 => MatchSettlementType.Normal,
3 => MatchSettlementType.Tutor,
4 => MatchSettlementType.Story,
_ => MatchSettlementType.None
};
}
[MemoryPackConstructor]
@ -45,7 +56,7 @@ namespace TH1_Logic.MatchConfig
public void Init(MapData map, MapConfig config)
{
SettlementType = config?.MatchSettlement ?? MatchSettlementType.None;
SettlementType = NormalizeType(config?.MatchSettlement ?? MatchSettlementType.None);
IsFinished = false;
PlayerSettlements ??= new Dictionary<uint, PlayerSettlementGroup>();
PlayerSettlements.Clear();
@ -67,7 +78,7 @@ namespace TH1_Logic.MatchConfig
public MatchSettlementInfo(MatchSettlementInfo copyData)
{
SettlementType = copyData.SettlementType;
SettlementType = NormalizeType(copyData.SettlementType);
IsFinished = copyData.IsFinished;
PlayerSettlements = new Dictionary<uint, PlayerSettlementGroup>();
foreach (var kv in copyData.PlayerSettlements)
@ -78,7 +89,7 @@ namespace TH1_Logic.MatchConfig
public void DeepCopy(MatchSettlementInfo copyData)
{
SettlementType = copyData.SettlementType;
SettlementType = NormalizeType(copyData.SettlementType);
IsFinished = copyData.IsFinished;
PlayerSettlements ??= new Dictionary<uint, PlayerSettlementGroup>();
PlayerSettlements.Clear();
@ -91,6 +102,7 @@ namespace TH1_Logic.MatchConfig
private static List<PlayerSettlementInfo> BuildProtectedSettlements(MapConfig config)
{
var protectedSettlements = new List<PlayerSettlementInfo>();
var matchType = NormalizeType(config?.MatchSettlement ?? MatchSettlementType.None);
var sourceSettlements = config?.PlayerSettlements;
if (sourceSettlements != null)
{
@ -104,15 +116,15 @@ namespace TH1_Logic.MatchConfig
protectedSettlements.Add(BuildProtectedSettlement(
sourceSettlement,
config?.MatchSettlement ?? MatchSettlementType.None));
matchType));
}
}
if (protectedSettlements.Count > 0) return protectedSettlements;
LogSystem.LogWarning(
$"MatchSettlement.Init: 结算配置为空或无效已注入默认结算任务。MatchType={config?.MatchSettlement}");
protectedSettlements.Add(CreateFallbackSettlement(config?.MatchSettlement ?? MatchSettlementType.None));
$"MatchSettlement.Init: 结算配置为空或无效已注入默认结算任务。MatchType={matchType}");
protectedSettlements.Add(CreateFallbackSettlement(matchType));
return protectedSettlements;
}
@ -232,15 +244,14 @@ namespace TH1_Logic.MatchConfig
{
private static Dictionary<MatchSettlementType, IMatchSettlement> _logicDict = new Dictionary<MatchSettlementType, IMatchSettlement>()
{
{ MatchSettlementType.Domination, new DominationMatchSettlement() },
{ MatchSettlementType.Perfect, new PerfectMatchSettlement() },
{ MatchSettlementType.Normal, new NormalMatchSettlement() },
{ MatchSettlementType.Tutor, new TutorMatchSettlement() },
{ MatchSettlementType.Story, new StoryMatchSettlement() },
{ MatchSettlementType.Creative, new StoryMatchSettlement() },
};
public static void RefreshMatchSettlementInfo(MapData map)
{
map.MatchSettlement.SettlementType = MatchSettlementInfo.NormalizeType(map.MatchSettlement.SettlementType);
if (!_logicDict.TryGetValue(map.MatchSettlement.SettlementType, out var logic))
{
LogSystem.LogError($"RefreshPlayerSettlementInfo Error TaskType:{map.MatchSettlement.SettlementType} No Logic");
@ -291,12 +302,12 @@ namespace TH1_Logic.MatchConfig
}
// DOMINATION 任意玩家结算且为胜,游戏结束 所有非AI玩家结算且均为败游戏结束
public class DominationMatchSettlement : IMatchSettlement
// NORMAL 任意玩家结算且为胜,游戏结束 所有非AI玩家结算且均为败游戏结束
public class NormalMatchSettlement : IMatchSettlement
{
public override MatchSettlementType GetSettlementType()
{
return MatchSettlementType.Domination;
return MatchSettlementType.Normal;
}
public override void Refresh(MapData map, MatchSettlementInfo info)
@ -314,37 +325,7 @@ namespace TH1_Logic.MatchConfig
{
if (map.CheckIsAI(kv.Key)) continue;
if (kv.Value.IsSettlement && !kv.Value.IsWin) continue;
MatchSettlementStuckGuard.CheckAndRecover(map, info, MatchSettlementType.Domination, kv.Key);
return;
}
info.IsFinished = true;
}
}
// PERFECT 所有非AI玩家均结算后游戏结束
public class PerfectMatchSettlement : IMatchSettlement
{
public override MatchSettlementType GetSettlementType()
{
return MatchSettlementType.Perfect;
}
public override void Refresh(MapData map, MatchSettlementInfo info)
{
if (info.IsFinished) return;
foreach (var kv in info.PlayerSettlements)
{
if (kv.Value.IsSettlement && kv.Value.IsWin)
{
info.IsFinished = true;
return;
}
}
foreach (var kv in info.PlayerSettlements)
{
if (map.CheckIsAI(kv.Key)) continue;
if (kv.Value.IsSettlement && !kv.Value.IsWin) continue;
MatchSettlementStuckGuard.CheckAndRecover(map, info, MatchSettlementType.Perfect, kv.Key);
MatchSettlementStuckGuard.CheckAndRecover(map, info, MatchSettlementType.Normal, kv.Key);
return;
}
info.IsFinished = true;
@ -435,38 +416,6 @@ namespace TH1_Logic.MatchConfig
}
}
// CREATIVE 所有非AI玩家均结算后游戏结束
public class CreativeMatchSettlement : IMatchSettlement
{
public override MatchSettlementType GetSettlementType()
{
return MatchSettlementType.Creative;
}
public override void Refresh(MapData map, MatchSettlementInfo info)
{
if (info.IsFinished) return;
foreach (var kv in info.PlayerSettlements)
{
if (kv.Value.IsSettlement && kv.Value.IsWin)
{
info.IsFinished = true;
return;
}
}
foreach (var kv in info.PlayerSettlements)
{
if (map.CheckIsAI(kv.Key)) continue;
if (kv.Value.IsSettlement && !kv.Value.IsWin) continue;
MatchSettlementStuckGuard.CheckAndRecover(map, info, MatchSettlementType.Creative, kv.Key);
return;
}
info.IsFinished = true;
}
}
/// <summary>
/// 偶现 bug 兜底:玩家明明已经满足"应当结束"的客观条件,但 task 链路因为某种运行时累积态没把
/// PlayerSettlementGroup 翻成 IsSettlement导致游戏一直能下一回合。已知"退出再读档"可恢复。

View File

@ -47,17 +47,43 @@ namespace TH1_Logic.MatchConfig
public void OnAfterMemoryPackDeserialize()
{
Items ??= new List<MomentItemBase>();
EnsureMomentItems();
}
public void InitItems()
{
Items.Clear();
AddAllMomentItems();
}
private void EnsureMomentItems()
{
for (int i = Items.Count - 1; i >= 0; i--)
{
if (Items[i] is ExploitWonderMomentItem)
Items.RemoveAt(i);
}
var existingTypes = new HashSet<System.Type>();
foreach (var item in Items)
{
if (item != null)
existingTypes.Add(item.GetType());
}
AddAllMomentItems(existingTypes);
}
private void AddAllMomentItems(HashSet<System.Type> existingTypes = null)
{
var baseType = typeof(MomentItemBase);
var assembly = baseType.Assembly;
foreach (var type in assembly.GetTypes())
{
if (type.IsClass && !type.IsAbstract && baseType.IsAssignableFrom(type))
{
if (type == typeof(ExploitWonderMomentItem)) continue;
if (existingTypes != null && existingTypes.Contains(type)) continue;
var item = (MomentItemBase)System.Activator.CreateInstance(type);
Items.Add(item);
}
@ -137,6 +163,13 @@ namespace TH1_Logic.MatchConfig
[MemoryPackUnion(56, typeof(ExploitFirstBigSawmillMomentItem))]
[MemoryPackUnion(57, typeof(BattleHasTwoHeroMomentItem))]
[MemoryPackUnion(58, typeof(BattleHasThreeHeroMomentItem))]
[MemoryPackUnion(59, typeof(ExploitWonderPeaceMomentItem))]
[MemoryPackUnion(60, typeof(ExploitWonderKnowledgeMomentItem))]
[MemoryPackUnion(61, typeof(ExploitWonderTradeMomentItem))]
[MemoryPackUnion(62, typeof(ExploitWonderWealthMomentItem))]
[MemoryPackUnion(63, typeof(ExploitWonderPowerMomentItem))]
[MemoryPackUnion(64, typeof(ExploitWonderParkMomentItem))]
[MemoryPackUnion(65, typeof(ExploitWonderEyeMomentItem))]
public abstract partial class MomentItemBase
{
public bool IsExecute = false;
@ -144,6 +177,16 @@ namespace TH1_Logic.MatchConfig
public int ExecuteCount = 0;
public abstract MomentSubType GetMomentSubType();
public virtual string GetFallbackDesc()
{
return string.Empty;
}
public virtual string GetProgressText(MapData map, PlayerData player)
{
return string.Empty;
}
public virtual void Execute(PlayerData player)
{
ExecuteCount++;
@ -844,17 +887,141 @@ namespace TH1_Logic.MatchConfig
[MemoryPackable]
public partial class ExploitWonderMomentItem : MomentItemBase
{
public ExploitWonderMomentItem()
{
PlayerCultureValue = 5;
}
public override MomentSubType GetMomentSubType() => MomentSubType.ExploitWonder;
public override void OnWonderBuild(MapData map, PlayerData player, WonderTypeEnum wonderType)
{
}
}
public abstract partial class ExploitSpecificWonderMomentItem : MomentItemBase
{
protected abstract WonderTypeEnum WonderType { get; }
protected abstract string FallbackDesc { get; }
public ExploitSpecificWonderMomentItem()
{
PlayerCultureValue = 5;
}
public override string GetFallbackDesc()
{
return FallbackDesc;
}
public override void OnWonderBuild(MapData map, PlayerData player, WonderTypeEnum wonderType)
{
if (IsExecute) return;
if (wonderType != WonderType) return;
IsExecute = true;
Execute(player);
}
public override string GetProgressText(MapData map, PlayerData player)
{
if (map == null || player == null) return string.Empty;
if (IsExecute) return string.Empty;
switch (WonderType)
{
case WonderTypeEnum.PEACE:
return $"{ClampProgress(player.TurnNoAttack, 5)}/5";
case WonderTypeEnum.KNOWLEDGE:
return GetTechProgress(player);
case WonderTypeEnum.TRADE:
return $"{ClampProgress(Main.PlayerLogic.GetConnectedCityCount(map, player), 5)}/5";
case WonderTypeEnum.WEALTH:
return $"{ClampProgress(player.PlayerCoin, 100)}/100";
case WonderTypeEnum.POWER:
return $"{ClampProgress(player.TotalKill, 10)}/10";
case WonderTypeEnum.PARK:
return $"{ClampProgress(Main.PlayerLogic.GetMaxCityLevel(map, player), 6)}/6";
case WonderTypeEnum.EYE:
return $"{GetTowerInSightCount(map, player)}/4";
default:
return string.Empty;
}
}
private static int ClampProgress(int value, int max)
{
if (value < 0) return 0;
return value > max ? max : value;
}
private static string GetTechProgress(PlayerData player)
{
if (player.TechTree.HasAllTech(player)) return "1/1";
return "0/1";
}
private static int GetTowerInSightCount(MapData map, PlayerData player)
{
int count = 0;
if (map.GridMap.GetGridDataByPos(0, 0, out var g1) && player.Sight.CheckIsInSight(g1.Id)) count++;
if (map.GridMap.GetGridDataByPos(0, (int)map.MapConfig.Height - 1, out var g2) && player.Sight.CheckIsInSight(g2.Id)) count++;
if (map.GridMap.GetGridDataByPos((int)map.MapConfig.Width - 1, 0, out var g3) && player.Sight.CheckIsInSight(g3.Id)) count++;
if (map.GridMap.GetGridDataByPos((int)map.MapConfig.Width - 1, (int)map.MapConfig.Height - 1, out var g4) && player.Sight.CheckIsInSight(g4.Id)) count++;
return count;
}
}
[MemoryPackable]
public partial class ExploitWonderPeaceMomentItem : ExploitSpecificWonderMomentItem
{
protected override WonderTypeEnum WonderType => WonderTypeEnum.PEACE;
protected override string FallbackDesc => "完成和平奇观";
public override MomentSubType GetMomentSubType() => MomentSubType.ExploitWonderPEACE;
}
[MemoryPackable]
public partial class ExploitWonderKnowledgeMomentItem : ExploitSpecificWonderMomentItem
{
protected override WonderTypeEnum WonderType => WonderTypeEnum.KNOWLEDGE;
protected override string FallbackDesc => "完成知识奇观";
public override MomentSubType GetMomentSubType() => MomentSubType.ExploitWonderKNOWLEDGE;
}
[MemoryPackable]
public partial class ExploitWonderTradeMomentItem : ExploitSpecificWonderMomentItem
{
protected override WonderTypeEnum WonderType => WonderTypeEnum.TRADE;
protected override string FallbackDesc => "完成贸易奇观";
public override MomentSubType GetMomentSubType() => MomentSubType.ExploitWonderTRADE;
}
[MemoryPackable]
public partial class ExploitWonderWealthMomentItem : ExploitSpecificWonderMomentItem
{
protected override WonderTypeEnum WonderType => WonderTypeEnum.WEALTH;
protected override string FallbackDesc => "完成财富奇观";
public override MomentSubType GetMomentSubType() => MomentSubType.ExploitWonderWEALTH;
}
[MemoryPackable]
public partial class ExploitWonderPowerMomentItem : ExploitSpecificWonderMomentItem
{
protected override WonderTypeEnum WonderType => WonderTypeEnum.POWER;
protected override string FallbackDesc => "完成力量奇观";
public override MomentSubType GetMomentSubType() => MomentSubType.ExploitWonderPOWER;
}
[MemoryPackable]
public partial class ExploitWonderParkMomentItem : ExploitSpecificWonderMomentItem
{
protected override WonderTypeEnum WonderType => WonderTypeEnum.PARK;
protected override string FallbackDesc => "完成乐园奇观";
public override MomentSubType GetMomentSubType() => MomentSubType.ExploitWonderPARK;
}
[MemoryPackable]
public partial class ExploitWonderEyeMomentItem : ExploitSpecificWonderMomentItem
{
protected override WonderTypeEnum WonderType => WonderTypeEnum.EYE;
protected override string FallbackDesc => "完成结界塔奇观";
public override MomentSubType GetMomentSubType() => MomentSubType.ExploitWonderEYE;
}
@ -1145,4 +1312,4 @@ namespace TH1_Logic.MatchConfig
IsExecute = true;
}
}
}
}

View File

@ -63,6 +63,8 @@ namespace TH1_Logic.Steam
// 监听套接字
private float _socketRecord;
private HSteamListenSocket _listenSocket;
private float _lastConnectionFailureTime = -999f;
private string _lastConnectionFailureReason = string.Empty;
// 回调
private Callback<SteamNetConnectionStatusChangedCallback_t> _cbConnectionStatusChanged;
@ -76,6 +78,11 @@ namespace TH1_Logic.Steam
public event Action<string> OnConnectionErrorEvent;
public bool IsInitialized => _listenSocket != HSteamListenSocket.Invalid;
public bool HasRecentConnectionFailure(float seconds, out string reason)
{
reason = _lastConnectionFailureReason;
return _lastConnectionFailureTime > 0f && Time.time - _lastConnectionFailureTime <= seconds;
}
// 初始化
public void Initialize()
@ -107,6 +114,8 @@ namespace TH1_Logic.Steam
_listenSocket = SteamNetworkingSockets.CreateListenSocketP2P(TargetPort, 0, null);
if (_listenSocket != HSteamListenSocket.Invalid)
{
_lastConnectionFailureTime = -999f;
_lastConnectionFailureReason = string.Empty;
LogSystem.LogInfo($"成功在虚拟端口 {TargetPort} 创建监听套接字: {_listenSocket}");
}
}
@ -176,7 +185,9 @@ namespace TH1_Logic.Steam
var connection = SteamNetworkingSockets.ConnectP2P(ref identity, TargetPort, options.Length, options);
if (connection == HSteamNetConnection.Invalid)
{
OnConnectionErrorEvent?.Invoke($"Failed to connect to {steamID}");
var reason = $"Failed to connect to {steamID}";
MarkConnectionFailure(reason);
OnConnectionErrorEvent?.Invoke(reason);
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.P2PConnectionFailed);
return false;
}
@ -283,6 +294,7 @@ namespace TH1_Logic.Steam
foreach (var steamID in timeoutList)
{
var reason = $"Connection to {steamID} timed out";
MarkConnectionFailure(reason);
LogSystem.LogError(reason);
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.P2PConnectionTimeout);
_connectionTimeouts.Remove(steamID);
@ -404,6 +416,8 @@ namespace TH1_Logic.Steam
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connected:
LogSystem.LogInfo($"Successfully connected to {remote}");
_lastConnectionFailureTime = -999f;
_lastConnectionFailureReason = string.Empty;
if (!_connections.ContainsKey(remote))
{
_connections[remote] = info.m_hConn;
@ -422,6 +436,7 @@ namespace TH1_Logic.Steam
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
LogSystem.LogWarning($"Connection problem detected locally: {remote}");
LogSystem.LogWarning($"Problem details: {info.m_info.m_szEndDebug}");
MarkConnectionFailure($"Connection problem detected locally: {remote}, {info.m_info.m_szEndDebug}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.P2PConnectionFailed);
HandleDisconnection(remote, info.m_hConn);
break;
@ -430,6 +445,7 @@ namespace TH1_Logic.Steam
// 检查具体的失败原因
var endReason = info.m_info.m_eEndReason;
LogSystem.LogError($"Connection failed - Reason: {endReason}");
MarkConnectionFailure($"Connection failed: {remote}, reason: {endReason}");
var isTimeout = endReason == (int)ESteamNetConnectionEnd.k_ESteamNetConnectionEnd_Misc_Timeout
|| endReason == (int)ESteamNetConnectionEnd.k_ESteamNetConnectionEnd_Remote_Timeout;
NetworkPlayerTipManager.Instance.Request(isTimeout
@ -469,6 +485,12 @@ namespace TH1_Logic.Steam
}
// 检查是否应该接受这个连接
private void MarkConnectionFailure(string reason)
{
_lastConnectionFailureTime = Time.time;
_lastConnectionFailureReason = reason ?? string.Empty;
}
private bool ShouldAcceptIncomingConnection(CSteamID steamID)
{
// 基本检查确保是有效的Steam ID

View File

@ -578,6 +578,31 @@ namespace TH1_Logic.Steam
|| details.m_eAvail == ESteamNetworkingAvailability.k_ESteamNetworkingAvailability_Current;
return relayOk;
}
public bool CanCreateLobbyNow(out string reason)
{
reason = string.Empty;
if (!IsSteamSessionLikelyAlive())
{
reason = "Steam session is not alive.";
return false;
}
if (SimpleP2P.Instance == null || !SimpleP2P.Instance.IsInitialized)
{
reason = "P2P listen socket is not ready.";
return false;
}
if (SimpleP2P.Instance.HasRecentConnectionFailure(30f, out var p2pReason))
{
reason = p2pReason;
return false;
}
return true;
}
// 检查 steam_appid.txt 文件
private void CheckSteamAppIdFile()
@ -636,6 +661,13 @@ namespace TH1_Logic.Steam
public void CreateLobby(int maxMembers = 4, bool isPublic = true, string password = "", string roomName = "")
{
if (!EnsureSteamReadyForLobbyAction(nameof(CreateLobby))) return;
if (!CanCreateLobbyNow(out var reason))
{
LogSystem.LogWarning($"Cannot create lobby now: {reason}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return;
}
if (CurrentState != LobbyState.None)
{
LogSystem.LogInfo($"Cannot create lobby in state: {CurrentState}");
@ -644,7 +676,11 @@ namespace TH1_Logic.Steam
_pendingLobbyPassword = password ?? "";
_pendingLobbyRoomName = FilterRoomName(string.IsNullOrWhiteSpace(roomName) ? GetDefaultRoomName(SelfName) : roomName.Trim());
_pendingLobbyIsPublic = isPublic;
if (_pendingLobbyRoomName.Length < 2)
_pendingLobbyRoomName = FilterRoomName(GetDefaultRoomName(SelfName));
if (_pendingLobbyRoomName.Length < 2)
_pendingLobbyRoomName = "Default";
_pendingLobbyIsPublic = isPublic;
CurrentState = LobbyState.Creating;
LogSystem.LogInfo($"Creating {(isPublic ? "public" : "friends-only")} lobby with max members: {maxMembers}, hasPassword: {!string.IsNullOrEmpty(_pendingLobbyPassword)}");
if (!TrySteamApi("SteamMatchmaking.CreateLobby", () => SteamMatchmaking.CreateLobby(isPublic?ELobbyType.k_ELobbyTypePublic:ELobbyType.k_ELobbyTypeFriendsOnly, maxMembers)))
@ -1352,7 +1388,11 @@ namespace TH1_Logic.Steam
if (!CurrentLobby.IsValid()) return false;
roomName = FilterRoomName(roomName);
roomName = FilterRoomName(string.IsNullOrWhiteSpace(roomName) ? GetRoomName() : roomName.Trim());
if (roomName.Length < 2)
roomName = FilterRoomName(GetRoomName());
if (roomName.Length < 2)
roomName = "Default";
bool success = SteamMatchmaking.SetLobbyData(CurrentLobby, "RoomName", roomName);
if (success)
{

View File

@ -28,9 +28,12 @@ namespace TH1_UI.View.Bottom
// ChatArea
public RectTransform ChatAreaRoot;
public RectTransform NetErrorPanelRoot;
private UIChatAreaMono _chatArea;
private GameObject _chatAreaGo;
private UINetErrorAreaMono _netErrorArea;
private GameObject _netErrorAreaGo;
private List<UIBottomNetRowMono> _netInfoRowList = null;
private bool _initialized = false;
@ -49,7 +52,7 @@ namespace TH1_UI.View.Bottom
UpdateView();
}
//Init将确定进入游戏时所有房间成员的状态除非有新成员加入否则不会删减
// Initializes the room member state when entering a match.
private void Init()
{
_initialized = true;
@ -58,11 +61,17 @@ namespace TH1_UI.View.Bottom
if (NetInfo != null)
NetInfo.gameObject.SetActive(isMulti);
// ChatArea 跟随 NetInfo 一起初始化(每局重新订阅 OnSendMessage
// ChatArea and NetErrorArea follow NetInfo lifetime.
if (isMulti)
{
InitChatArea();
InitNetErrorArea();
}
else
{
CloseChatArea();
CloseNetErrorArea();
}
if (!isMulti) return;
@ -107,7 +116,7 @@ namespace TH1_UI.View.Bottom
ScrollViewRect.sizeDelta = sd;
}
//UpdateView只会修改NetStatus和房主标记
// UpdateView only changes NetStatus and room owner marker.
public void UpdateView()
{
@ -123,7 +132,7 @@ namespace TH1_UI.View.Bottom
var mids = lobbyInfo.GetAllMemberInfo();
PlayMemberEnterAudioIfNeeded(mids.Count);
//如果发现人数变多了重新Init比如继续一个3人局游戏开始进来的时候只有2人
// Reinitialize if the lobby member count grows.
if (mids.Count > _netInfoRowList.Count)
Init();
@ -175,7 +184,7 @@ namespace TH1_UI.View.Bottom
var parent = ChatAreaRoot != null ? ChatAreaRoot : transform as RectTransform;
_chatAreaGo = Instantiate(prefab, parent);
FitChatAreaToRoot(_chatAreaGo.transform as RectTransform);
FitToRoot(_chatAreaGo.transform as RectTransform, parent);
_chatArea = _chatAreaGo.GetComponent<UIChatAreaMono>();
if (_chatArea == null)
{
@ -190,9 +199,9 @@ namespace TH1_UI.View.Bottom
_chatArea.Init();
}
private void FitChatAreaToRoot(RectTransform rectTransform)
private void FitToRoot(RectTransform rectTransform, RectTransform root)
{
if (rectTransform == null || ChatAreaRoot == null) return;
if (rectTransform == null || root == null) return;
rectTransform.anchorMin = Vector2.zero;
rectTransform.anchorMax = Vector2.one;
rectTransform.offsetMin = Vector2.zero;
@ -215,13 +224,67 @@ namespace TH1_UI.View.Bottom
_chatAreaGo = null;
}
private void InitNetErrorArea()
{
if (_netErrorArea != null) return;
var prefab = Resources.Load<GameObject>("Prefab/UI/Common/Chat/NetErrorAreaPanel");
if (prefab == null)
{
Debug.LogError("[UIBottomNet] NetErrorAreaPanel prefab not found.");
return;
}
var parent = GetNetErrorPanelRoot();
_netErrorAreaGo = Instantiate(prefab, parent);
FitToRoot(_netErrorAreaGo.transform as RectTransform, parent);
_netErrorAreaGo.transform.SetAsLastSibling();
_netErrorArea = _netErrorAreaGo.GetComponent<UINetErrorAreaMono>();
if (_netErrorArea == null)
{
Debug.LogError("[UIBottomNet] NetErrorAreaPanel missing UINetErrorAreaMono.");
Destroy(_netErrorAreaGo);
_netErrorAreaGo = null;
return;
}
_netErrorArea.Init();
}
private RectTransform GetNetErrorPanelRoot()
{
if (NetErrorPanelRoot != null) return NetErrorPanelRoot;
var root = transform.Find("NetErrorPanelRoot") as RectTransform;
if (root != null)
{
NetErrorPanelRoot = root;
return NetErrorPanelRoot;
}
return ChatAreaRoot != null ? ChatAreaRoot : transform as RectTransform;
}
private void CloseNetErrorArea()
{
if (_netErrorArea != null)
_netErrorArea.Shutdown();
if (_netErrorAreaGo != null)
Destroy(_netErrorAreaGo);
_netErrorArea = null;
_netErrorAreaGo = null;
}
public void CloseView()
{
_initialized = false;
_lastNetMemberCount = -1;
// 清理 ChatArea
// 娓呯悊 ChatArea
CloseChatArea();
CloseNetErrorArea();
//AudioManager.Instance.StopMusic();
}

View File

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Text;
using TH1_Logic.Net;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace TH1_UI.View.Common
{
public class UINetErrorAreaMono : MonoBehaviour
{
[Header("Toggle")]
public Button ToggleButton;
public GameObject LogArea;
[Header("Log List")]
public RectTransform Content;
public GameObject LogItemPrefab;
public ScrollRect ScrollRect;
[Header("Config")]
public int MaxLogCount = 100;
private readonly Queue<GameObject> _logItems = new Queue<GameObject>();
private bool _initialized;
private bool _logAreaVisible;
private void OnDestroy()
{
Shutdown();
}
public void Init()
{
if (_initialized) return;
_initialized = true;
if (ToggleButton != null)
{
ToggleButton.gameObject.SetActive(true);
ToggleButton.onClick.RemoveListener(ToggleLogArea);
ToggleButton.onClick.AddListener(ToggleLogArea);
}
SetLogAreaVisible(false);
NetworkPlayerTipManager.Instance.OnTipRequested += OnNetworkPlayerTipRequested;
}
public void Shutdown()
{
if (!_initialized) return;
_initialized = false;
NetworkPlayerTipManager.Instance.OnTipRequested -= OnNetworkPlayerTipRequested;
if (ToggleButton != null)
ToggleButton.onClick.RemoveListener(ToggleLogArea);
ClearLogs();
}
private void OnNetworkPlayerTipRequested(NetworkPlayerTipPayload payload)
{
AddLog(payload);
}
private void ToggleLogArea()
{
SetLogAreaVisible(!_logAreaVisible);
}
private void SetLogAreaVisible(bool visible)
{
_logAreaVisible = visible;
if (LogArea != null)
LogArea.SetActive(visible);
}
private void AddLog(NetworkPlayerTipPayload payload)
{
if (Content == null || LogItemPrefab == null)
{
Debug.LogWarning("[NetErrorArea] Log prefab or content is not assigned.");
return;
}
while (_logItems.Count >= Mathf.Max(1, MaxLogCount))
{
var oldest = _logItems.Dequeue();
if (oldest != null)
Destroy(oldest);
}
var go = Instantiate(LogItemPrefab, Content);
go.SetActive(true);
var text = go.GetComponentInChildren<TMP_Text>();
if (text != null)
text.text = BuildLogText(payload);
_logItems.Enqueue(go);
if (ScrollRect != null)
{
Canvas.ForceUpdateCanvases();
ScrollRect.verticalNormalizedPosition = 0f;
}
}
private static string BuildLogText(NetworkPlayerTipPayload payload)
{
var sb = new StringBuilder();
sb.Append('[').Append(DateTime.Now.ToString("HH:mm:ss")).Append("] ");
sb.Append(payload?.TipType.ToString() ?? "NetworkTip");
if (!string.IsNullOrEmpty(payload?.Title))
sb.Append(" | ").Append(payload.Title);
if (!string.IsNullOrEmpty(payload?.Message))
sb.Append('\n').Append(payload.Message);
return sb.ToString();
}
private void ClearLogs()
{
while (_logItems.Count > 0)
{
var go = _logItems.Dequeue();
if (go != null)
Destroy(go);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a2f7cc80c9bc4d179c6cbebdb94477bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -22,6 +22,7 @@ namespace TH1_UI.View.Info
private MomentItemBase _momentItem;
private MomentSubData _momentSubData;
private CultureTaskInfo _cultureTaskInfo;
public void Init(MomentItemBase momentItem)
{
@ -29,19 +30,29 @@ namespace TH1_UI.View.Info
if (_momentItem == null) return;
var subType = _momentItem.GetMomentSubType();
if (!Table.Instance.MomentDataAssets.GetMomentSubData(subType, out _momentSubData)) return;
Table.Instance.CultureCardDataAssets.GetCultureTaskInfo(subType, out _cultureTaskInfo);
Table.Instance.MomentDataAssets.GetMomentSubData(subType, out _momentSubData);
// 描述
if (Desc != null)
MultilingualManager.Instance.SetUIText(Desc, _momentSubData.Desc);
{
if (_cultureTaskInfo != null)
MultilingualManager.Instance.SetUIText(Desc, _cultureTaskInfo.Description);
else if (_momentSubData != null)
MultilingualManager.Instance.SetUIText(Desc, _momentSubData.Desc);
else
Desc.text = _momentItem.GetFallbackDesc();
}
// 图标
if (Icon != null && _momentSubData.Icon != null)
if (Icon != null && _cultureTaskInfo != null && _cultureTaskInfo.Icon != null)
Icon.sprite = _cultureTaskInfo.Icon;
else if (Icon != null && _momentSubData != null && _momentSubData.Icon != null)
Icon.sprite = _momentSubData.Icon;
// 文化值
if (CultureValue != null)
CultureValue.text = _momentItem.PlayerCultureValue.ToString();
CultureValue.text = (_cultureTaskInfo != null ? _cultureTaskInfo.CultureValue : _momentItem.PlayerCultureValue).ToString();
// 默认隐藏CompletedGroupPer由UpdateInfo根据条件决定是否显示
if (CompletedGroupPer != null)
@ -50,11 +61,13 @@ namespace TH1_UI.View.Info
public void UpdateInfo(PlayerData player)
{
if (_momentItem == null || _momentSubData == null) return;
if (_momentItem == null) return;
bool isEveryTime = _momentSubData.EveryTime;
bool isEveryTime = _momentSubData != null && _momentSubData.EveryTime;
bool oneTimeCompleted = _momentItem.IsExecute;
bool everyTimeCompleted = isEveryTime && _momentItem.ExecuteCount >= _momentSubData.MaxTime;
string progressText = _momentItem.GetProgressText(Main.MapData, player);
bool hasCustomProgress = !string.IsNullOrEmpty(progressText);
// 一次性显示AwardTitleEveryTime且未达MaxTime显示AwardTitlePer达到MaxTime两者都不显示
if (AwardTitle != null) AwardTitle.SetActive(!isEveryTime && !oneTimeCompleted);
@ -63,7 +76,12 @@ namespace TH1_UI.View.Info
// 执行次数仅EveryTime且未达MaxTime时显示格式为 "当前次数/最大次数"
if (ExecuteCountText != null)
{
if (isEveryTime && !everyTimeCompleted)
if (hasCustomProgress && !oneTimeCompleted)
{
ExecuteCountText.gameObject.SetActive(true);
ExecuteCountText.text = progressText;
}
else if (isEveryTime && !everyTimeCompleted)
{
ExecuteCountText.gameObject.SetActive(true);
ExecuteCountText.text = _momentItem.ExecuteCount + "/" + _momentSubData.MaxTime;

View File

@ -62,6 +62,15 @@ namespace TH1_UI.View.Notify
case UINotifyCommonType.OutsideMultiplayRoomGone:
MultilingualManager.Instance.SetUIText(content,Table.Instance.TextDataAssets.OutsideMultiplayRoomGone);
break;
case UINotifyCommonType.OutsideMultiplayRoomNetError:
MultilingualManager.Instance.SetUIText(content,Table.Instance.TextDataAssets.OutsideMultiplayRoomNetError);
break;
case UINotifyCommonType.OutsideMultiplayOpenHint:
MultilingualManager.Instance.SetUIText(content,Table.Instance.TextDataAssets.OutsideMultiplayOpenHint);
break;
case UINotifyCommonType.OutsideMultiplayCantStartCount:
MultilingualManager.Instance.SetUIText(content,Table.Instance.TextDataAssets.OutsideMultiplayCantStartCount);
break;
}
Timer.Instance.TimerRegister(this, AutoClose,1f,"UINotifyCommonView");
}

View File

@ -4,10 +4,18 @@ namespace TH1_UI.View.Outside
{
internal static class RoomNameInputValidator
{
public const int MinRoomNameLength = 2;
public const int MaxRoomNameLength = 20;
public const string InvalidRoomNameHint = "房间名称包含不适当内容,请修改后再确认。禁止使用辱骂、歧视、色情、广告、冒充官方或泄露隐私等内容。";
public const string InvalidCharactersHint = "房间名称只能使用中文、英文字母、数字、下划线、中划线和点。";
public enum ErrorType
{
None,
Illegal,
TooShort
}
public static string SanitizeAllowedCharacters(string input)
{
if (string.IsNullOrEmpty(input)) return string.Empty;
@ -32,20 +40,38 @@ namespace TH1_UI.View.Outside
}
public static bool TryValidateForSubmit(string input, string fallbackRoomName, out string roomName, out string hint)
{
ErrorType errorType;
if (TryValidateForSubmitWithErrorType(input, fallbackRoomName, out roomName, out errorType))
{
hint = null;
return true;
}
hint = errorType == ErrorType.TooShort ? string.Empty : InvalidRoomNameHint;
return false;
}
public static bool TryValidateForSubmitWithErrorType(string input, string fallbackRoomName, out string roomName, out ErrorType errorType)
{
roomName = SanitizeAllowedCharacters(input);
if (string.IsNullOrEmpty(roomName)) roomName = SanitizeAllowedCharacters(fallbackRoomName);
if (string.IsNullOrEmpty(roomName)) roomName = "Default";
if (roomName.Length < MinRoomNameLength)
{
errorType = ErrorType.TooShort;
return false;
}
var filtered = BannedWordFilter.Filter(roomName, BannedTextContext.Name);
if (filtered != roomName)
{
hint = InvalidRoomNameHint;
errorType = ErrorType.Illegal;
return false;
}
hint = null;
errorType = ErrorType.None;
return true;
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using Logic.Multilingual;
using TH1_Logic.Steam;
using TMPro;
using UnityEngine;
@ -68,6 +69,7 @@ namespace TH1_UI.View.Outside
InitRoomNameInput();
EnsureSeatCountOption();
AutoBindPasswordLabel();
AutoBindRoomNameHintText();
EnsurePasswordHint();
}
@ -94,9 +96,10 @@ namespace TH1_UI.View.Outside
{
args = default;
var inputRoomName = RoomNameInput != null ? RoomNameInput.text : string.Empty;
if (!RoomNameInputValidator.TryValidateForSubmit(inputRoomName, _defaultRoomName, out var roomName, out var roomNameHint))
RoomNameInputValidator.ErrorType roomNameError;
if (!RoomNameInputValidator.TryValidateForSubmitWithErrorType(inputRoomName, _defaultRoomName, out var roomName, out roomNameError))
{
ShowRoomNameHint(roomNameHint);
ShowRoomNameHint(roomNameError);
return false;
}
@ -197,14 +200,19 @@ namespace TH1_UI.View.Outside
RoomNameInput.SetTextWithoutNotify(sanitized);
RoomNameInput.caretPosition = sanitized.Length;
ShowRoomNameHint(RoomNameInputValidator.InvalidCharactersHint);
ShowRoomNameHint(RoomNameInputValidator.ErrorType.Illegal);
}
private void ShowRoomNameHint(string hint)
private void ShowRoomNameHint(RoomNameInputValidator.ErrorType errorType)
{
EnsureRoomNameHint();
if (RoomNameHintText != null) RoomNameHintText.text = hint;
if (RoomNameHintObject != null) RoomNameHintObject.SetActive(true);
if (RoomNameHintObject == null || RoomNameHintText == null)
{
Debug.LogError("[UIOutsideMultiplayCreateRoomPanel] RoomNameHint is not assigned.");
return;
}
RoomNameHintObject.SetActive(true);
MultilingualManager.Instance.SetUIText(RoomNameHintText, GetRoomNameHintTextKey(errorType));
RebuildPanelLayout();
}
@ -327,28 +335,28 @@ namespace TH1_UI.View.Outside
PasswordHintObject.SetActive(false);
}
private void EnsureRoomNameHint()
private void AutoBindRoomNameHintText()
{
if (RoomNameHintObject != null && RoomNameHintText != null) return;
if (RoomNameInput == null || RoomNameInput.textComponent == null) return;
if (RoomNameHintObject == null)
{
var hint = transform.Find("RoomNameHint");
if (hint == null && RoomNameInput != null && RoomNameInput.transform.parent != null)
hint = RoomNameInput.transform.parent.Find("RoomNameHint");
if (hint == null && PanelRoot != null)
hint = PanelRoot.transform.Find("RoomNameHint");
if (hint != null)
RoomNameHintObject = hint.gameObject;
}
var hintObject = new GameObject("RoomNameHint", typeof(RectTransform));
hintObject.transform.SetParent(RoomNameInput.transform.parent, false);
var rect = hintObject.GetComponent<RectTransform>();
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = new Vector2(0f, -34f);
rect.sizeDelta = new Vector2(520f, 54f);
if (RoomNameHintObject != null && RoomNameHintText == null)
RoomNameHintText = RoomNameHintObject.GetComponentInChildren<TextMeshProUGUI>(true);
}
RoomNameHintText = hintObject.AddComponent<TextMeshProUGUI>();
RoomNameHintText.font = RoomNameInput.textComponent.font;
RoomNameHintText.fontSize = 18f;
RoomNameHintText.color = Color.red;
RoomNameHintText.alignment = TextAlignmentOptions.Center;
RoomNameHintText.text = RoomNameInputValidator.InvalidRoomNameHint;
RoomNameHintObject = hintObject;
RoomNameHintObject.SetActive(false);
private string GetRoomNameHintTextKey(RoomNameInputValidator.ErrorType errorType)
{
return errorType == RoomNameInputValidator.ErrorType.TooShort
? Table.Instance.TextDataAssets.OutsideMultiplayRoomNameTooShort
: Table.Instance.TextDataAssets.OutsideMultiplayRoomNameIllegal;
}
private int GetSeatCount()

View File

@ -15,6 +15,7 @@ public class UIOutsideMultiplayLobbyRowMono : MonoBehaviour
public TextMeshProUGUI VersionText;
public Button JoinButton;
public Button ActionButton;
public GameObject Lock;
private LobbyListInfo _lobbyInfo;
@ -48,6 +49,8 @@ public class UIOutsideMultiplayLobbyRowMono : MonoBehaviour
GameStateText.text = lobbyInfo.GameState.ToString();
if (VersionText != null)
VersionText.text = lobbyInfo.Version ?? "";
if (Lock != null)
Lock.SetActive(lobbyInfo.HasPassword);
JoinButton.onClick.RemoveAllListeners();
JoinButton.onClick.AddListener(() => onJoinClicked?.Invoke(_lobbyInfo));
@ -62,15 +65,29 @@ public class UIOutsideMultiplayLobbyRowMono : MonoBehaviour
private void AutoBindButtons()
{
if (ActionButton == null)
ActionButton = FindButton("ActionButton") ?? FindButton("MoreButton") ?? FindButton("OptionButton");
ActionButton = FindButton("ActionButton") ?? FindButton("MoreButton") ?? FindButton("OptionButton") ?? FindButton("JubaoButton");
if (Lock == null)
Lock = FindChild("Lock");
}
private Button FindButton(string buttonName)
{
var child = transform.Find(buttonName);
var child = FindChild(buttonName);
return child != null ? child.GetComponent<Button>() : null;
}
private GameObject FindChild(string childName)
{
for (int i = 0; i < transform.childCount; i++)
{
var child = transform.GetChild(i);
if (child.name == childName)
return child.gameObject;
}
return null;
}
void Start()
{
}

View File

@ -39,6 +39,7 @@ public class UIOutsideMultiplayMemberRowMono : MonoBehaviour
private string _forceNameOverride;
private Action<int> _onForceChanged;
private Action<int> _onTeamChanged;
private bool _showTeamControls = true;
private bool _useAllForceOptions;
private bool _shrinkFallbackLeaderAvatar;
private Vector3 _leaderImageDefaultScale = Vector3.one;
@ -77,13 +78,15 @@ public class UIOutsideMultiplayMemberRowMono : MonoBehaviour
bool canEdit,
string forceNameOverride,
Action<int> onForceChanged,
Action<int> onTeamChanged)
Action<int> onTeamChanged,
bool showTeamControls = true)
{
if (!CheckParam()) return;
ResetButtons();
SetRootBackground(false);
_lobby = lobby;
_forceNameOverride = forceNameOverride;
_showTeamControls = showTeamControls;
_teamId = Mathf.Max(1, teamId);
_maxTeamId = Mathf.Max(1, maxTeamId);
_onForceChanged = onForceChanged;
@ -118,6 +121,7 @@ public class UIOutsideMultiplayMemberRowMono : MonoBehaviour
SetOpenHintVisible(true);
StatusText.text = string.Empty;
ForcesText.text = string.Empty;
_showTeamControls = false;
SetTeamText(0);
SetEditButtonsVisible(false);
if (QuitButton != null) QuitButton.gameObject.SetActive(false);
@ -131,12 +135,14 @@ public class UIOutsideMultiplayMemberRowMono : MonoBehaviour
bool canEdit,
string forceNameOverride,
Action<int> onForceChanged,
Action<int> onTeamChanged)
Action<int> onTeamChanged,
bool showTeamControls = true)
{
if (!CheckParam()) return;
ResetButtons();
SetRootBackground(false);
_forceNameOverride = forceNameOverride;
_showTeamControls = showTeamControls;
_teamId = Mathf.Max(1, teamId);
_maxTeamId = Mathf.Max(1, maxTeamId);
_onForceChanged = onForceChanged;
@ -164,12 +170,14 @@ public class UIOutsideMultiplayMemberRowMono : MonoBehaviour
bool canEdit,
string forceNameOverride,
Action<int> onForceChanged,
Action<int> onTeamChanged)
Action<int> onTeamChanged,
bool showTeamControls = true)
{
if (!CheckParam()) return;
ResetButtons();
SetRootBackground(true);
_forceNameOverride = forceNameOverride;
_showTeamControls = showTeamControls;
_teamId = Mathf.Max(1, teamId);
_maxTeamId = Mathf.Max(1, maxTeamId);
_onForceChanged = onForceChanged;
@ -300,6 +308,13 @@ public class UIOutsideMultiplayMemberRowMono : MonoBehaviour
private void SetTeamText(int teamId)
{
if (TeamText == null) return;
if (!_showTeamControls)
{
TeamText.text = string.Empty;
TeamText.gameObject.SetActive(false);
return;
}
if (teamId <= 0)
{
TeamText.text = string.Empty;
@ -316,9 +331,9 @@ public class UIOutsideMultiplayMemberRowMono : MonoBehaviour
if (RefreshButton != null && RefreshButton != ForcesButton) RefreshButton.gameObject.SetActive(visible);
if (ForceLeftButton != null) ForceLeftButton.gameObject.SetActive(visible);
if (ForceRightButton != null) ForceRightButton.gameObject.SetActive(visible);
if (TeamLeftButton != null) TeamLeftButton.gameObject.SetActive(visible);
if (TeamRightButton != null) TeamRightButton.gameObject.SetActive(visible);
if (TeamText != null) TeamText.gameObject.SetActive(visible || !string.IsNullOrEmpty(TeamText.text));
if (TeamLeftButton != null) TeamLeftButton.gameObject.SetActive(_showTeamControls && visible);
if (TeamRightButton != null) TeamRightButton.gameObject.SetActive(_showTeamControls && visible);
if (TeamText != null) TeamText.gameObject.SetActive(_showTeamControls && (visible || !string.IsNullOrEmpty(TeamText.text)));
if (ForcesText != null) ForcesText.gameObject.SetActive(visible || !string.IsNullOrEmpty(ForcesText.text));
}
@ -328,8 +343,8 @@ public class UIOutsideMultiplayMemberRowMono : MonoBehaviour
if (ForceLeftButton != null) ForceLeftButton.onClick.AddListener(() => ChangeForce(-1));
if (ForceRightButton != null) ForceRightButton.onClick.AddListener(() => ChangeForce(1));
if (ForcesButton != null && ForceLeftButton == null && ForceRightButton == null) ForcesButton.onClick.AddListener(OnClickForces);
if (TeamLeftButton != null) TeamLeftButton.onClick.AddListener(() => ChangeTeam(-1));
if (TeamRightButton != null) TeamRightButton.onClick.AddListener(() => ChangeTeam(1));
if (_showTeamControls && TeamLeftButton != null) TeamLeftButton.onClick.AddListener(() => ChangeTeam(-1));
if (_showTeamControls && TeamRightButton != null) TeamRightButton.onClick.AddListener(() => ChangeTeam(1));
}
private void BindQuitButton(MemberInfo info, SteamLobbyManager lobby)

View File

@ -12,10 +12,10 @@ namespace TH1_UI.View.Outside
public Button ReportButton;
private LobbyListInfo _lobbyInfo;
private Action<LobbyListInfo> _onBlock;
private Action<LobbyListInfo> _onReport;
private Func<LobbyListInfo, bool> _onBlock;
private Func<LobbyListInfo, bool> _onReport;
public void Init(Action<LobbyListInfo> onBlock, Action<LobbyListInfo> onReport)
public void Init(Func<LobbyListInfo, bool> onBlock, Func<LobbyListInfo, bool> onReport)
{
_onBlock = onBlock;
_onReport = onReport;
@ -54,14 +54,14 @@ namespace TH1_UI.View.Outside
private void OnClickBlock()
{
Hide();
_onBlock?.Invoke(_lobbyInfo);
if (_onBlock?.Invoke(_lobbyInfo) == true)
Hide();
}
private void OnClickReport()
{
Hide();
_onReport?.Invoke(_lobbyInfo);
if (_onReport?.Invoke(_lobbyInfo) == true)
Hide();
}
}
}

View File

@ -42,10 +42,6 @@ namespace TH1_UI.View.Outside
public TextMeshProUGUI RoomSeatText;
[Header("相关提示对象")]
public GameObject CantStartHint;
public TextMeshProUGUI CantStartHintText;
public AnimancerComponent CantStartHintAnimancer;
public Button CantStartHintCloseButton;
public GameObject NoRoomHint;
public GameObject NotOwnerHint;
@ -88,9 +84,13 @@ namespace TH1_UI.View.Outside
public UIOutsideMultiplayJoinPasswordPanelMono JoinPasswordPanelMono;
public GameObject RoomActionPanel;
public UIOutsideMultiplayRoomActionPanelMono RoomActionPanelMono;
public GameObject MultiplayInsideNotify;
public TextMeshProUGUI MultiplayInsideNotifyText;
public CanvasGroup MultiplayInsideNotifyCanvasGroup;
[Header("聊天区域")]
public RectTransform ChatAreaRoot;
public RectTransform NetErrorAreaRoot;
[Header("房间设置选项组")]
@ -117,9 +117,14 @@ namespace TH1_UI.View.Outside
private List<UIOutsideMultiplayLobbyRowMono> _lobbyRowList;
private UIChatAreaMono _chatArea;
private GameObject _chatAreaGo;
private UINetErrorAreaMono _netErrorArea;
private GameObject _netErrorAreaGo;
private LobbyListInfo _pendingJoinLobbyInfo;
private int _multiplayInsideNotifyVersion;
private SteamLobbyManager _lobby;
private const string MutedRoomNamesPrefsKey = "OutsideMultiplayMutedRoomNames";
private const long MutedRoomNameDurationSeconds = 24 * 60 * 60;
private const int MaxRoomSeatCount = 4;
private int _roomSeatCount = 4;
private int _openMemberRowCount = 0;
@ -164,6 +169,19 @@ namespace TH1_UI.View.Outside
(CivEnum.Indian, ForceEnum.Satori),
};
[Serializable]
private class MutedRoomNameRecord
{
public string RoomName;
public long ExpireAtUnixSeconds;
}
[Serializable]
private class MutedRoomNameStore
{
public List<MutedRoomNameRecord> Records = new List<MutedRoomNameRecord>();
}
protected override void OnInit()
{
base.OnInit();
@ -260,22 +278,12 @@ namespace TH1_UI.View.Outside
_lobbyRowList = new List<UIOutsideMultiplayLobbyRowMono>();
_lobby = LobbyManager.Instance.Lobby as SteamLobbyManager;
EnsureMultiplayInsideNotify();
EnsureMultiplaySubPanels();
RefreshHallButton.onClick.RemoveAllListeners();
RefreshHallButton.onClick.AddListener(OnRefreshLobbyClicked);
// 注册CantStartHint关闭按钮
CantStartHintCloseButton.onClick.RemoveAllListeners();
CantStartHintCloseButton.onClick.AddListener(() =>
{
var fadeOutAnim = ResourceCache.Instance.AnimCache.UICommonPanelFadeOut;
CantStartHintAnimancer.Play(fadeOutAnim).Events.OnEnd = () =>
{
CantStartHint.SetActive(false);
};
});
//_inLobby = _lobby.IsInLobby();
}
@ -295,6 +303,8 @@ namespace TH1_UI.View.Outside
public void SetContent(ShowUIOutsideMultiplay evt)
{
InitNetErrorArea();
//Step #1 设置朋友列表
RefreshFriendList();
//Step #2 设置房间列表
@ -394,6 +404,7 @@ namespace TH1_UI.View.Outside
else
{
InitChatArea();
InitNetErrorArea();
_noRoom = false;
PlayMemberEnterAudioIfNeeded();
SetRoomInfoSetting();
@ -473,6 +484,7 @@ namespace TH1_UI.View.Outside
int roomSeatCount = Mathf.Clamp(_roomSeatCount, humanRows.Count, Mathf.Min(totalPlayerCount, MaxRoomSeatCount));
_openMemberRowCount = Mathf.Max(0, roomSeatCount - humanRows.Count);
int aiCount = Mathf.Max(0, totalPlayerCount - roomSeatCount);
bool showAiRows = IsTeamAndAIConfigEnabled();
_roomMemberRows.AddRange(humanRows);
for (int i = 0; i < _openMemberRowCount; i++)
@ -483,10 +495,13 @@ namespace TH1_UI.View.Outside
_roomMemberRows.Add(new RoomMemberRowData { Type = RoomMemberRowType.Open });
}
_roomMemberRows.Add(new RoomMemberRowData { Type = RoomMemberRowType.Line });
int aiStart = Mathf.Max(0, emptyAiRows.Count - aiCount);
for (int i = aiStart; i < emptyAiRows.Count; i++)
_roomMemberRows.Add(emptyAiRows[i]);
if (showAiRows && aiCount > 0)
{
_roomMemberRows.Add(new RoomMemberRowData { Type = RoomMemberRowType.Line });
int aiStart = Mathf.Max(0, emptyAiRows.Count - aiCount);
for (int i = aiStart; i < emptyAiRows.Count; i++)
_roomMemberRows.Add(emptyAiRows[i]);
}
}
private void RenderRoomMemberRows(List<MemberCiv> multiCivs)
@ -547,18 +562,21 @@ namespace TH1_UI.View.Outside
var teamId = GetValidTeamId(mc);
var forceNameOverride = GetForceNameOverride(mc, civ, force, multiCivs, sameCountDict);
int maxTeamId = Mathf.Max(1, (int)Main.Instance.MapConfig.PlayerCount);
bool showTeamControls = IsTeamAndAIConfigEnabled();
if (row.Type == RoomMemberRowType.Open)
{
memberRow.SetOpenContent(civ, force, teamId, maxTeamId, _lobby.IsLobbyOwner(), forceNameOverride,
direction => OnOpenRowForceChanged(row.SlotIndex, direction),
direction => OnOpenRowTeamChanged(row.SlotIndex, direction));
direction => OnOpenRowTeamChanged(row.SlotIndex, direction),
showTeamControls);
return;
}
if (row.Type == RoomMemberRowType.AI)
{
memberRow.SetAIContent(civ, force, teamId, maxTeamId, _lobby.IsLobbyOwner(), forceNameOverride,
direction => OnAIRowForceChanged(row.SlotIndex, direction),
direction => OnAIRowTeamChanged(row.SlotIndex, direction));
direction => OnAIRowTeamChanged(row.SlotIndex, direction),
showTeamControls);
return;
}
@ -566,7 +584,8 @@ namespace TH1_UI.View.Outside
bool canEditHuman = mc.MemberId == _lobby.GetSelfMemberId();
memberRow.SetHumanContent(row.MemberInfo, status, civ, force, teamId, maxTeamId, _lobby, canEditHuman, forceNameOverride,
direction => OnHumanRowForceChanged(mc.Index, direction),
direction => OnHumanRowTeamChanged(mc.Index, direction));
direction => OnHumanRowTeamChanged(mc.Index, direction),
showTeamControls);
}
private UIOutsideMultiplayMemberRowMono GetMemberRow(int index)
@ -687,7 +706,7 @@ namespace TH1_UI.View.Outside
private int GetMinRoomSeatCount()
{
return Mathf.Max(2, _lobby.GetMemberCount());
return Mathf.Max(1, _lobby.GetMemberCount());
}
private int GetMaxRoomSeatCount()
@ -730,6 +749,7 @@ namespace TH1_UI.View.Outside
bool slotLayoutChanged = false;
var usedCivs = new HashSet<uint>();
var usedTeams = new HashSet<int>();
bool forceSoloTeams = !IsTeamAndAIConfigEnabled();
for (int i = 0; i < multiCivs.Count; i++)
{
var mc = multiCivs[i];
@ -744,7 +764,16 @@ namespace TH1_UI.View.Outside
}
if (shouldBeOpenSlot) openSlotBudget--;
}
if (mc.TeamId <= 0 || mc.TeamId > totalPlayerCount) mc.TeamId = PickDefaultTeamId(i, totalPlayerCount, usedTeams);
int soloTeamId = GetSoloTeamId(i, totalPlayerCount);
if (forceSoloTeams && mc.TeamId != soloTeamId)
{
mc.TeamId = soloTeamId;
slotLayoutChanged = true;
}
else if (!forceSoloTeams && (mc.TeamId <= 0 || mc.TeamId > totalPlayerCount))
{
mc.TeamId = PickDefaultTeamId(i, totalPlayerCount, usedTeams);
}
if (!mc.IsCivFixed)
{
var civId = PickDefaultCivId(i, usedCivs);
@ -762,6 +791,16 @@ namespace TH1_UI.View.Outside
}
}
private bool IsTeamAndAIConfigEnabled()
{
return Main.Instance?.MapConfig?.GameMode == RuntimeData.GameMode.CREATIVE;
}
private static int GetSoloTeamId(int slotIndex, int totalPlayerCount)
{
return Mathf.Clamp(slotIndex + 1, 1, Mathf.Max(1, totalPlayerCount));
}
private int GetValidTeamId(MemberCiv mc)
{
int maxTeamId = Mathf.Max(1, (int)Main.Instance.MapConfig.PlayerCount);
@ -809,6 +848,7 @@ namespace TH1_UI.View.Outside
private void OnHumanRowTeamChanged(int slotIndex, int direction)
{
if (!IsTeamAndAIConfigEnabled()) return;
var slot = Main.Instance.MapConfig.GetPlayerSlot(slotIndex, NetMode.Multi);
if (slot == null) return;
int nextTeamId = GetNextTeamId(GetValidTeamId(slot), direction);
@ -835,6 +875,7 @@ namespace TH1_UI.View.Outside
private void OnAIRowTeamChanged(int slotIndex, int direction)
{
if (!IsTeamAndAIConfigEnabled()) return;
if (!_lobby.IsLobbyOwner()) return;
var slot = Main.Instance.MapConfig.GetPlayerSlot(slotIndex, NetMode.Multi);
if (slot == null) return;
@ -873,6 +914,7 @@ namespace TH1_UI.View.Outside
private void SetEmptyPlayerSlotTeam(int slotIndex, int direction)
{
if (!IsTeamAndAIConfigEnabled()) return;
if (!_lobby.IsLobbyOwner()) return;
var slot = Main.Instance.MapConfig.GetPlayerSlot(slotIndex, NetMode.Multi);
if (slot == null || slot.MemberId != 0 || slot.IsAI) return;
@ -1049,9 +1091,10 @@ namespace TH1_UI.View.Outside
}
var inputRoomName = RoomNameInput != null ? RoomNameInput.text : string.Empty;
if (!RoomNameInputValidator.TryValidateForSubmit(inputRoomName, _lobby.GetRoomName(), out var roomName, out var roomNameHint))
RoomNameInputValidator.ErrorType roomNameError;
if (!RoomNameInputValidator.TryValidateForSubmitWithErrorType(inputRoomName, _lobby.GetRoomName(), out var roomName, out roomNameError))
{
ShowRoomNameHint(roomNameHint);
ShowRoomNameHint(MultilingualManager.Instance.GetMultilingualText(GetRoomNameHintTextKey(roomNameError)));
return;
}
@ -1106,6 +1149,13 @@ namespace TH1_UI.View.Outside
if (RoomNameHintObject != null) RoomNameHintObject.SetActive(false);
}
private string GetRoomNameHintTextKey(RoomNameInputValidator.ErrorType errorType)
{
return errorType == RoomNameInputValidator.ErrorType.TooShort
? Table.Instance.TextDataAssets.OutsideMultiplayRoomNameTooShort
: Table.Instance.TextDataAssets.OutsideMultiplayRoomNameIllegal;
}
private void EnsureRoomNameHint()
{
if (RoomNameHintObject != null && RoomNameHintText != null) return;
@ -1136,6 +1186,13 @@ namespace TH1_UI.View.Outside
if (!_lobby.IsLobbyOwner()) return;
// CREATIVE 模式下 MapSize 由玩家自由选择,不再被 PlayerCount 联动
if (GetPlayerCountByOptionIndex(idx) < _roomSeatCount)
{
PlayerCount.Select(GetPlayerCountOptionIndex(Main.Instance.MapConfig.PlayerCount));
ShowLobbyNotify(UINotifyCommonType.OutsideMultiplayCantStartCount);
return;
}
if (GameMode != null && GameMode.SelectedIndex == 2)
{
SetMapConfig(resetGuestReady: true);
@ -1155,6 +1212,16 @@ namespace TH1_UI.View.Outside
RoomSettingOnPlayerCountClicked(Main.Instance.MapConfig.PlayerCount,Main.Instance.MapConfig.Width);
}
private static uint GetPlayerCountByOptionIndex(uint optionIndex)
{
return optionIndex + 2;
}
private static uint GetPlayerCountOptionIndex(uint playerCount)
{
return playerCount >= 2 ? playerCount - 2 : 0;
}
public void OnDiffOptionClicked(uint idx)
{
if (!_lobby.IsLobbyOwner()) return;
@ -1254,8 +1321,7 @@ namespace TH1_UI.View.Outside
{
if (NoRoomHint == null) return;
var lobbyInfos = _lobby.LobbyListInfos;
int lobbyCount = lobbyInfos?.Count ?? 0;
int lobbyCount = GetVisibleLobbyInfos().Count;
//搜索中隐藏NoRoomHint
//搜索完成且有房间隐藏NoRoomHint
@ -1279,6 +1345,8 @@ namespace TH1_UI.View.Outside
public void CreateRoom()
{
if (!CanCreateRoomNow()) return;
//如果已经在lobby了刷新界面
if (_lobby.IsInLobby())
{
@ -1300,11 +1368,28 @@ namespace TH1_UI.View.Outside
}
}
private bool CanCreateRoomNow()
{
string reason = string.Empty;
if (_lobby != null && _lobby.CanCreateLobbyNow(out reason))
return true;
if (!string.IsNullOrEmpty(reason))
Debug.LogWarning($"[UIOutsideMultiplay] Cannot create room: {reason}");
CreateRoomPanelMono?.Hide();
CreateRoomPanel?.SetActive(false);
ShowLobbyNotify(UINotifyCommonType.OutsideMultiplayRoomNetError);
return false;
}
/// <summary>
/// 根据选择的房间类型创建房间
/// </summary>
public void CreateRoomWithType()
{
if (!CanCreateRoomNow()) return;
//隐藏创建房间面板
CreateRoomPanel?.SetActive(false);
@ -1314,16 +1399,18 @@ namespace TH1_UI.View.Outside
// 旧入口兜底:没有绑定新面板 Mono 时仍能创建公开/好友房。
bool isPublic = RoomType?.SelectedIndex != 1;
_roomSeatCount = Mathf.Clamp(_roomSeatCount, 2, 4);
_roomSeatCount = Mathf.Clamp(_roomSeatCount, 1, 4);
_lobby.CreateLobby(_roomSeatCount, isPublic, string.Empty, SteamLobbyManager.GetDefaultRoomName(_lobby.SelfName));
}
private void OnCreateRoomPanelConfirm(UIOutsideMultiplayCreateRoomPanelMono.CreateRoomParams args)
{
if (!CanCreateRoomNow()) return;
_lobby.OnLobbyEnteredEvent += OnLobbyJoinSuccess;
_lobby.OnLobbyErrorEvent += OnLobbyJoinFailed;
_roomSeatCount = Mathf.Clamp(args.SeatCount, 2, 4);
_roomSeatCount = Mathf.Clamp(args.SeatCount, 1, 4);
_lobby.CreateLobby(_roomSeatCount, args.IsPublic, args.Password, args.RoomName);
}
@ -1351,7 +1438,7 @@ namespace TH1_UI.View.Outside
if (_openMemberRowCount > 0)
{
ShowCantStartHint(Table.Instance.TextDataAssets.OutsideMultiplayOpenHint);
ShowLobbyNotify(UINotifyCommonType.OutsideMultiplayOpenHint);
return;
}
@ -1376,28 +1463,13 @@ namespace TH1_UI.View.Outside
if (members.Count > playerCount)
{
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyMembersNotSynced);
ShowCantStartHint(Table.Instance.TextDataAssets.OutsideMultiplayCantStartCount);
ShowLobbyNotify(UINotifyCommonType.OutsideMultiplayCantStartCount);
return;
}
ShowLoadingAndStartGame(false);
}
private void ShowCantStartHint(string textKey)
{
CantStartHint.SetActive(true);
CantStartHintText.text = MultilingualManager.Instance.GetMultilingualText(textKey);
CantStartHintAnimancer.Play(ResourceCache.Instance.AnimCache.UICommonPanelFadeIn);
Timer.Instance.TimerRegister(this, () =>
{
CantStartHintAnimancer.Play(ResourceCache.Instance.AnimCache.UICommonPanelFadeOut);
},1f,"MultiplayStartGame");
Timer.Instance.TimerRegister(this, () =>
{
CantStartHint.SetActive(false);
},1f + ResourceCache.Instance.AnimCache.UICommonPanelFadeOut.length,"MultiplayStartGame");
}
private void SetMapConfig(bool resetGuestReady = false)
{
@ -1533,12 +1605,13 @@ namespace TH1_UI.View.Outside
public void OnGameModeOptionClicked(uint idx)
{
if (!_lobby.IsLobbyOwner()) return;
OnGameModeOptionClickedInternal(idx, isOwner: true, syncConfig: true);
bool wasCreative = Main.Instance?.MapConfig?.GameMode == RuntimeData.GameMode.CREATIVE;
OnGameModeOptionClickedInternal(idx, isOwner: true, syncConfig: true, resetTeamsWhenLeavingCreative: wasCreative && idx != 2);
}
// 统一处理 GameMode 的 Passive 联动SetRoomInfoSetting 初始化/手动点击 共用)
// syncConfig=true 时才写 MapConfig 并广播Init 流程只刷 Passive避免重复广播
private void OnGameModeOptionClickedInternal(uint idx, bool isOwner, bool syncConfig)
private void OnGameModeOptionClickedInternal(uint idx, bool isOwner, bool syncConfig, bool resetTeamsWhenLeavingCreative = false)
{
bool isCreative = idx == 2;
if (WinRow != null) WinRow.SetActive(isCreative);
@ -1588,6 +1661,8 @@ namespace TH1_UI.View.Outside
if (syncConfig)
{
SetMapConfig(resetGuestReady: true);
if (resetTeamsWhenLeavingCreative)
ResetAllSlotsToSoloTeams();
// 复用既有房主→成员广播入口CheckMapConfigChanged 内部已调用)
RoomSettingOnPlayerCountClicked(Main.Instance.MapConfig.PlayerCount, Main.Instance.MapConfig.Width);
}
@ -1599,6 +1674,30 @@ namespace TH1_UI.View.Outside
SetMapConfig(resetGuestReady: true);
Main.Instance.MapConfig.CheckMapConfigChanged();
}
private bool ResetAllSlotsToSoloTeams()
{
var mapConfig = Main.Instance?.MapConfig;
if (mapConfig == null) return false;
mapConfig.SetPlayerCount(mapConfig.PlayerCount, NetMode.Multi);
var multiCivs = mapConfig.MultiCivs;
if (multiCivs == null) return false;
int totalPlayerCount = Mathf.Max(1, (int)mapConfig.PlayerCount);
bool changed = false;
for (int i = 0; i < multiCivs.Count; i++)
{
var mc = multiCivs[i];
if (mc == null) continue;
int soloTeamId = GetSoloTeamId(i, totalPlayerCount);
if (mc.TeamId == soloTeamId) continue;
mc.TeamId = soloTeamId;
changed = true;
}
return changed;
}
private void InitChatArea()
@ -1617,8 +1716,7 @@ namespace TH1_UI.View.Outside
return;
}
_chatAreaGo = Instantiate(prefab);
_chatAreaGo.transform.SetParent(ChatAreaRoot, false);
_chatAreaGo = Instantiate(prefab, ChatAreaRoot, false);
_chatAreaGo.transform.SetAsLastSibling();
FitChatAreaToRoot(_chatAreaGo.transform as RectTransform);
_chatArea = _chatAreaGo.GetComponent<UIChatAreaMono>();
@ -1662,6 +1760,48 @@ namespace TH1_UI.View.Outside
_chatAreaGo = null;
}
private void InitNetErrorArea()
{
if (_netErrorArea != null) return;
if (NetErrorAreaRoot == null)
{
Debug.LogError("[UIOutsideMultiplay] NetErrorAreaRoot is not assigned.");
return;
}
var prefab = Resources.Load<GameObject>("Prefab/UI/Common/Chat/NetErrorAreaPanel");
if (prefab == null)
{
Debug.LogError("[UIOutsideMultiplay] NetErrorAreaPanel prefab not found.");
return;
}
_netErrorAreaGo = Instantiate(prefab, NetErrorAreaRoot, false);
_netErrorAreaGo.transform.SetAsLastSibling();
_netErrorArea = _netErrorAreaGo.GetComponent<UINetErrorAreaMono>();
if (_netErrorArea == null)
{
Debug.LogError("[UIOutsideMultiplay] NetErrorAreaPanel missing UINetErrorAreaMono.");
Destroy(_netErrorAreaGo);
_netErrorAreaGo = null;
return;
}
_netErrorArea.Init();
}
private void CloseNetErrorArea()
{
if (_netErrorArea != null)
_netErrorArea.Shutdown();
if (_netErrorAreaGo != null)
Destroy(_netErrorAreaGo);
_netErrorArea = null;
_netErrorAreaGo = null;
}
private void OnChatMessageSendInternal(string message)
{
var lobby = LobbyManager.Instance.Lobby;
@ -1673,6 +1813,8 @@ namespace TH1_UI.View.Outside
public void OnCloseView()
{
CloseChatArea();
CloseNetErrorArea();
HideMultiplayInsideNotify();
_lobby.OnMembersChangedEvent -= RefreshAll;
_lobby.OnLobbyLeftEvent -= RefreshAll;
@ -1734,8 +1876,8 @@ namespace TH1_UI.View.Outside
{
if (!_lobby.IsInitialized()) return;
var lobbyInfos = _lobby.LobbyListInfos;
int lobbyCount = lobbyInfos?.Count ?? 0;
var lobbyInfos = GetVisibleLobbyInfos();
int lobbyCount = lobbyInfos.Count;
//动态补足行对象遵循现有FriendRow模式
while (_lobbyRowList.Count < lobbyCount)
@ -1817,28 +1959,262 @@ namespace TH1_UI.View.Outside
private void OnLobbyActionClicked(LobbyListInfo lobbyInfo)
{
EnsureMultiplaySubPanels();
RoomActionPanelMono?.Show(lobbyInfo);
}
private void OnBlockLobbyClicked(LobbyListInfo lobbyInfo)
private bool OnBlockLobbyClicked(LobbyListInfo lobbyInfo)
{
Debug.Log($"[UIOutsideMultiplay] Block lobby owner: {lobbyInfo?.OwnerId}");
if (!MuteLobbyRoomName(lobbyInfo))
return false;
RefreshLobbyList();
UpdateNoRoomHintVisibility();
ShowMultiplayInsideNotify(Table.Instance.TextDataAssets.OutsideMultiplayRoomMuteSuccess);
Debug.Log($"[UIOutsideMultiplay] Muted lobby room name: {GetLobbyMuteRoomName(lobbyInfo)}");
return true;
}
private void OnReportLobbyClicked(LobbyListInfo lobbyInfo)
private bool OnReportLobbyClicked(LobbyListInfo lobbyInfo)
{
if (lobbyInfo == null)
return false;
Debug.Log($"[UIOutsideMultiplay] Report lobby owner: {lobbyInfo?.OwnerId}");
EventManager.Publish(new ShowUIGlobalBugReport());
ShowMultiplayInsideNotify(Table.Instance.TextDataAssets.OutsideMultiplayRoomJubaoSuccess);
return true;
}
private void ShowLobbyNotify(UINotifyCommonType type)
{
EventManager.Publish(new ShowUINotifyCommon { UINotifyCommonType = type });
switch (type)
{
case UINotifyCommonType.OutsideMultiplayRoomFull:
ShowMultiplayInsideNotify(Table.Instance.TextDataAssets.OutsideMultiplayRoomFull);
break;
case UINotifyCommonType.OutsideMultiplayRoomGone:
ShowMultiplayInsideNotify(Table.Instance.TextDataAssets.OutsideMultiplayRoomGone);
break;
case UINotifyCommonType.OutsideMultiplayRoomNetError:
ShowMultiplayInsideNotify(Table.Instance.TextDataAssets.OutsideMultiplayRoomNetError);
break;
case UINotifyCommonType.OutsideMultiplayOpenHint:
ShowMultiplayInsideNotify(Table.Instance.TextDataAssets.OutsideMultiplayOpenHint);
break;
case UINotifyCommonType.OutsideMultiplayCantStartCount:
ShowMultiplayInsideNotify(Table.Instance.TextDataAssets.OutsideMultiplayCantStartCount);
break;
default:
EventManager.Publish(new ShowUINotifyCommon { UINotifyCommonType = type });
break;
}
}
private void ShowMultiplayInsideNotify(string textKey)
{
EnsureMultiplayInsideNotify();
if (MultiplayInsideNotify == null)
return;
if (MultiplayInsideNotifyText != null)
SetTextDataAssetText(MultiplayInsideNotifyText, textKey);
if (MultiplayInsideNotifyCanvasGroup != null)
MultiplayInsideNotifyCanvasGroup.alpha = 1f;
int notifyVersion = ++_multiplayInsideNotifyVersion;
MultiplayInsideNotify.SetActive(true);
Timer.Instance.TimerRegister(this, () =>
{
if (_multiplayInsideNotifyVersion == notifyVersion)
HideMultiplayInsideNotify();
}, 2f, "MultiplayInsideNotify");
}
private void SetTextDataAssetText(TextMeshProUGUI text, string textKey)
{
if (text == null)
return;
if (uint.TryParse(textKey, out _))
{
MultilingualManager.Instance.SetUIText(text, textKey);
return;
}
text.text = textKey ?? string.Empty;
}
private void HideMultiplayInsideNotify()
{
if (MultiplayInsideNotify != null)
MultiplayInsideNotify.SetActive(false);
}
private void EnsureMultiplayInsideNotify()
{
if (MultiplayInsideNotify == null)
{
var existing = transform.Find("MultiplayInsideNotify");
MultiplayInsideNotify = existing != null ? existing.gameObject : null;
}
if (MultiplayInsideNotify == null)
return;
if (MultiplayInsideNotifyText == null)
{
var textTransform = MultiplayInsideNotify.transform.Find("Text");
MultiplayInsideNotifyText = textTransform != null
? textTransform.GetComponent<TextMeshProUGUI>()
: MultiplayInsideNotify.GetComponentInChildren<TextMeshProUGUI>(true);
}
if (MultiplayInsideNotifyCanvasGroup == null)
MultiplayInsideNotifyCanvasGroup = MultiplayInsideNotify.GetComponent<CanvasGroup>();
MultiplayInsideNotify.SetActive(false);
}
private List<LobbyListInfo> GetVisibleLobbyInfos()
{
var visibleLobbyInfos = new List<LobbyListInfo>();
var lobbyInfos = _lobby?.LobbyListInfos;
if (lobbyInfos == null)
return visibleLobbyInfos;
var store = LoadMutedRoomNameStore();
long now = GetNowUnixSeconds();
bool changed = PruneExpiredMutedRoomNames(store, now);
for (int i = 0; i < lobbyInfos.Count; i++)
{
if (!IsLobbyRoomNameMuted(lobbyInfos[i], store, now))
visibleLobbyInfos.Add(lobbyInfos[i]);
}
if (changed)
SaveMutedRoomNameStore(store);
return visibleLobbyInfos;
}
private bool MuteLobbyRoomName(LobbyListInfo lobbyInfo)
{
string roomName = GetLobbyMuteRoomName(lobbyInfo);
if (string.IsNullOrWhiteSpace(roomName))
return false;
var store = LoadMutedRoomNameStore();
long now = GetNowUnixSeconds();
PruneExpiredMutedRoomNames(store, now);
for (int i = store.Records.Count - 1; i >= 0; i--)
{
var record = store.Records[i];
if (record == null || string.Equals(record.RoomName, roomName, StringComparison.Ordinal))
store.Records.RemoveAt(i);
}
store.Records.Add(new MutedRoomNameRecord
{
RoomName = roomName,
ExpireAtUnixSeconds = now + MutedRoomNameDurationSeconds
});
SaveMutedRoomNameStore(store);
return true;
}
private bool IsLobbyRoomNameMuted(LobbyListInfo lobbyInfo, MutedRoomNameStore store, long now)
{
string roomName = GetLobbyMuteRoomName(lobbyInfo);
if (string.IsNullOrWhiteSpace(roomName) || store?.Records == null)
return false;
for (int i = 0; i < store.Records.Count; i++)
{
var record = store.Records[i];
if (record != null
&& record.ExpireAtUnixSeconds > now
&& string.Equals(record.RoomName, roomName, StringComparison.Ordinal))
return true;
}
return false;
}
private string GetLobbyMuteRoomName(LobbyListInfo lobbyInfo)
{
if (lobbyInfo == null)
return string.Empty;
string roomName = string.IsNullOrWhiteSpace(lobbyInfo.RoomName)
? $"Room {lobbyInfo.LobbyId}"
: lobbyInfo.RoomName;
return SteamLobbyManager.FilterRoomName(roomName).Trim();
}
private MutedRoomNameStore LoadMutedRoomNameStore()
{
string json = PlayerPrefs.GetString(MutedRoomNamesPrefsKey, string.Empty);
if (string.IsNullOrWhiteSpace(json))
return new MutedRoomNameStore();
try
{
var store = JsonUtility.FromJson<MutedRoomNameStore>(json);
if (store == null)
store = new MutedRoomNameStore();
if (store.Records == null)
store.Records = new List<MutedRoomNameRecord>();
return store;
}
catch (Exception e)
{
Debug.LogWarning($"[UIOutsideMultiplay] Failed to load muted room names: {e.Message}");
return new MutedRoomNameStore();
}
}
private void SaveMutedRoomNameStore(MutedRoomNameStore store)
{
if (store == null)
store = new MutedRoomNameStore();
if (store.Records == null)
store.Records = new List<MutedRoomNameRecord>();
PlayerPrefs.SetString(MutedRoomNamesPrefsKey, JsonUtility.ToJson(store));
PlayerPrefs.Save();
}
private bool PruneExpiredMutedRoomNames(MutedRoomNameStore store, long now)
{
if (store?.Records == null)
return false;
bool changed = false;
for (int i = store.Records.Count - 1; i >= 0; i--)
{
var record = store.Records[i];
if (record == null || string.IsNullOrWhiteSpace(record.RoomName) || record.ExpireAtUnixSeconds <= now)
{
store.Records.RemoveAt(i);
changed = true;
}
}
return changed;
}
private long GetNowUnixSeconds()
{
return DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
private void EnsureMultiplaySubPanels()
{
if (JoinPasswordPanelMono == null)
if (!IsValidSubPanelInstance(JoinPasswordPanel))
{
if (JoinPasswordPanel == null)
{
@ -1846,19 +2222,22 @@ namespace TH1_UI.View.Outside
JoinPasswordPanel = existing != null ? existing.gameObject : null;
}
if (JoinPasswordPanel == null)
if (!IsValidSubPanelInstance(JoinPasswordPanel))
{
var prefab = Resources.Load<GameObject>("Prefab/UI/Outside/UIOutsideMultiplayJoinPasswordPanel");
var prefab = JoinPasswordPanel;
if (prefab == null)
prefab = Resources.Load<GameObject>("Prefab/UI/Outside/UIOutsideMultiplayJoinPasswordPanel");
if (prefab != null)
JoinPasswordPanel = Instantiate(prefab, transform);
}
JoinPasswordPanelMono = JoinPasswordPanel?.GetComponent<UIOutsideMultiplayJoinPasswordPanelMono>();
}
if (JoinPasswordPanelMono == null || JoinPasswordPanelMono.gameObject != JoinPasswordPanel)
JoinPasswordPanelMono = JoinPasswordPanel?.GetComponent<UIOutsideMultiplayJoinPasswordPanelMono>();
JoinPasswordPanelMono?.Init(OnJoinPasswordConfirmed);
if (RoomActionPanelMono == null)
if (!IsValidSubPanelInstance(RoomActionPanel))
{
if (RoomActionPanel == null)
{
@ -1866,19 +2245,27 @@ namespace TH1_UI.View.Outside
RoomActionPanel = existing != null ? existing.gameObject : null;
}
if (RoomActionPanel == null)
if (!IsValidSubPanelInstance(RoomActionPanel))
{
var prefab = Resources.Load<GameObject>("Prefab/UI/Outside/UIOutsideMultiplayRoomActionPanel");
var prefab = RoomActionPanel;
if (prefab == null)
prefab = Resources.Load<GameObject>("Prefab/UI/Outside/UIOutsideMultiplayRoomActionPanel");
if (prefab != null)
RoomActionPanel = Instantiate(prefab, transform);
}
RoomActionPanelMono = RoomActionPanel?.GetComponent<UIOutsideMultiplayRoomActionPanelMono>();
}
if (RoomActionPanelMono == null || RoomActionPanelMono.gameObject != RoomActionPanel)
RoomActionPanelMono = RoomActionPanel?.GetComponent<UIOutsideMultiplayRoomActionPanelMono>();
RoomActionPanelMono?.Init(OnBlockLobbyClicked, OnReportLobbyClicked);
}
private bool IsValidSubPanelInstance(GameObject panel)
{
return panel != null && panel.scene.IsValid() && panel.transform.IsChildOf(transform);
}
/// <summary>
/// 加入成功回调
/// </summary>
@ -1902,9 +2289,7 @@ namespace TH1_UI.View.Outside
//显示加入失败提示
CantStartHint.SetActive(true);
var fadeInAnim = ResourceCache.Instance.AnimCache.UICommonPanelFadeIn;
CantStartHintAnimancer.Play(fadeInAnim);
ShowLobbyNotify(UINotifyCommonType.OutsideMultiplayRoomNetError);
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff