M5-M7: сцены, лобби, экран игры и цикл матча
Обе сцены собраны через Code Mode, не руками. - loader: поднимает модули, прогревает арт карт, уходит в lobby - lobby: обе UI-ветки в одной сцене (по ARCHITECTURE.md), матч — это смена видимого корня, а не загрузка сцены - GameScreen рисует текущее состояние и доводит узлы твинами, а не проигрывает поток событий: узел карты живёт вместе со своей картой, поэтому "шестёрка ушла из руки на стол" не требует отдельного случая - один тап = ход целиком; логика вынесена в core/rules/intent.ts и покрыта тестами, в том числе проверкой, что она никогда не предложит ход, который правила отвергнут - арт карт переехал в assets/resources: .pac — это инструкция упаковки на сборке, а не рантайм-ассет, и без ссылок на кадры в сборку не попадало ничего. Теперь все 37 спрайтов в одной текстуре 2048x2048 - клики кнопок подписываются в коде, а не через clickEvents сцены - строки интерфейса собраны в одном файле Сборка web-mobile проходит чисто, все ссылки в сцене проверены. Вживую не запускалось: браузер в этом окружении не поднимает главный цикл движка. Подробности — в docs/TECHNICAL.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@@ -0,0 +1,171 @@
|
||||
[
|
||||
{
|
||||
"__type__": "cc.Prefab",
|
||||
"_name": "card",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_native": "",
|
||||
"data": {
|
||||
"__id__": 1
|
||||
},
|
||||
"optimizationPolicy": 0,
|
||||
"persistent": false
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "card",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": null,
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 2
|
||||
},
|
||||
{
|
||||
"__id__": 4
|
||||
},
|
||||
{
|
||||
"__id__": 6
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 8
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 3
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 220,
|
||||
"height": 320
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "5332t7MZRMZ7fwhsKNiE8i"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Sprite",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 5
|
||||
},
|
||||
"_customMaterial": null,
|
||||
"_srcBlendFactor": 2,
|
||||
"_dstBlendFactor": 4,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 255,
|
||||
"g": 255,
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_spriteFrame": {
|
||||
"__uuid__": "610347f5-8934-4a68-8e68-70269c912a7a@f9941",
|
||||
"__expectedType__": "cc.SpriteFrame"
|
||||
},
|
||||
"_type": 0,
|
||||
"_fillType": 0,
|
||||
"_sizeMode": 0,
|
||||
"_fillCenter": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"_fillStart": 0,
|
||||
"_fillRange": 0,
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "9bawLq5CJJCLV+Gk6r0mbF"
|
||||
},
|
||||
{
|
||||
"__type__": "da016ez+jBIsZXjka0QzaXY",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 7
|
||||
},
|
||||
"sprite": {
|
||||
"__id__": 4
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "85phkdNBxMuaolENti1wG5"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
"__id__": 1
|
||||
},
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "f4cMBOjypJJZjyWxfFGIq6",
|
||||
"instance": null,
|
||||
"targetOverrides": null
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"ver": "1.1.50",
|
||||
"importer": "prefab",
|
||||
"imported": true,
|
||||
"uuid": "1ad36ebe-345c-4936-be65-c838f51b4eb4",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"syncNodeName": "card"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "a300e972-9887-4462-8c4d-47fe8ce9c106",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"isBundle": true,
|
||||
"bundleConfigID": "default",
|
||||
"bundleName": "resources",
|
||||
"priority": 8
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
@@ -3,7 +3,9 @@
|
||||
"importer": "auto-atlas",
|
||||
"imported": true,
|
||||
"uuid": "914afa5f-3f77-4d7d-835e-530e298c6816",
|
||||
"files": [],
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"maxWidth": 2048,
|
||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,881 @@
|
||||
[
|
||||
{
|
||||
"__type__": "cc.SceneAsset",
|
||||
"_name": "loader",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_native": "",
|
||||
"scene": {
|
||||
"__id__": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Scene",
|
||||
"_name": "loader",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": null,
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 2
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"autoReleaseAssets": false,
|
||||
"_globals": {
|
||||
"__id__": 19
|
||||
},
|
||||
"_id": "1aee5910-40c3-4b99-bc8d-e477cf0e25ed"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Canvas",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 3
|
||||
},
|
||||
{
|
||||
"__id__": 6
|
||||
},
|
||||
{
|
||||
"__id__": 10
|
||||
},
|
||||
{
|
||||
"__id__": 13
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 16
|
||||
},
|
||||
{
|
||||
"__id__": 17
|
||||
},
|
||||
{
|
||||
"__id__": 18
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 640,
|
||||
"y": 360,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "b1zlEjeP1PhKkvGqJs9BIU"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Camera",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 4
|
||||
},
|
||||
{
|
||||
"__id__": 5
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 1000
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "2a7WNH7jhEM6VozsNzw40D"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 3
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 100,
|
||||
"height": 100
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "eazUhZVnFKVo+ewS5T/BhE"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Camera",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 3
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_projection": 0,
|
||||
"_priority": 1073741824,
|
||||
"_fov": 45,
|
||||
"_fovAxis": 0,
|
||||
"_orthoHeight": 440.25052192066806,
|
||||
"_near": 1,
|
||||
"_far": 2000,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 8,
|
||||
"g": 44,
|
||||
"b": 26,
|
||||
"a": 255
|
||||
},
|
||||
"_depth": 1,
|
||||
"_stencil": 0,
|
||||
"_clearFlags": 7,
|
||||
"_rect": {
|
||||
"__type__": "cc.Rect",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 1,
|
||||
"height": 1
|
||||
},
|
||||
"_aperture": 19,
|
||||
"_shutter": 7,
|
||||
"_iso": 0,
|
||||
"_screenScale": 1,
|
||||
"_visibility": 33554432,
|
||||
"_targetTexture": null,
|
||||
"_postProcess": null,
|
||||
"_usePostProcess": false,
|
||||
"_cameraType": -1,
|
||||
"_trackingType": 0,
|
||||
"_id": "8b1/mYGuxP9KcC4z0D4cNh"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Background",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 7
|
||||
},
|
||||
{
|
||||
"__id__": 8
|
||||
},
|
||||
{
|
||||
"__id__": 9
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "71vPMl7sZIb5K5fhEuSsMZ"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 6
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 1280,
|
||||
"height": 720
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "b13TjyVF1EFoHFL0w6g3gL"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Sprite",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 6
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_customMaterial": null,
|
||||
"_srcBlendFactor": 2,
|
||||
"_dstBlendFactor": 4,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 255,
|
||||
"g": 255,
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_spriteFrame": {
|
||||
"__uuid__": "d5553c08-e820-46af-a832-40d807724dfb@f9941",
|
||||
"__expectedType__": "cc.SpriteFrame"
|
||||
},
|
||||
"_type": 0,
|
||||
"_fillType": 0,
|
||||
"_sizeMode": 0,
|
||||
"_fillCenter": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"_fillStart": 0,
|
||||
"_fillRange": 0,
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": "1eZC1dDqZKBKdiIGlCVQdh"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Widget",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 6
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_alignFlags": 45,
|
||||
"_target": null,
|
||||
"_left": 0,
|
||||
"_right": 0,
|
||||
"_top": 0,
|
||||
"_bottom": 0,
|
||||
"_horizontalCenter": 0,
|
||||
"_verticalCenter": 0,
|
||||
"_isAbsLeft": true,
|
||||
"_isAbsRight": true,
|
||||
"_isAbsTop": true,
|
||||
"_isAbsBottom": true,
|
||||
"_isAbsHorizontalCenter": true,
|
||||
"_isAbsVerticalCenter": true,
|
||||
"_originalWidth": 540,
|
||||
"_originalHeight": 960,
|
||||
"_alignMode": 1,
|
||||
"_lockFlags": 0,
|
||||
"_id": "6fkN296q1Ef5voPy2qYK6X"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Title",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 11
|
||||
},
|
||||
{
|
||||
"__id__": 12
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 60,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "e2HX8VID5CHqTyzON7WJb5"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 10
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 900,
|
||||
"height": 160
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "d2e/FNyWJMm4yQCG2+A4LP"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 10
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_customMaterial": null,
|
||||
"_srcBlendFactor": 2,
|
||||
"_dstBlendFactor": 4,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 244,
|
||||
"g": 194,
|
||||
"b": 69,
|
||||
"a": 255
|
||||
},
|
||||
"_string": "Fool",
|
||||
"_horizontalAlign": 1,
|
||||
"_verticalAlign": 1,
|
||||
"_actualFontSize": 127,
|
||||
"_fontSize": 130,
|
||||
"_fontFamily": "Arial",
|
||||
"_lineHeight": 163,
|
||||
"_overflow": 2,
|
||||
"_enableWrapText": false,
|
||||
"_font": null,
|
||||
"_isSystemFontUsed": true,
|
||||
"_spacingX": 0,
|
||||
"_isItalic": false,
|
||||
"_isBold": true,
|
||||
"_isUnderline": false,
|
||||
"_underlineHeight": 2,
|
||||
"_cacheMode": 0,
|
||||
"_enableOutline": false,
|
||||
"_outlineColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 255
|
||||
},
|
||||
"_outlineWidth": 2,
|
||||
"_enableShadow": false,
|
||||
"_shadowColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 255
|
||||
},
|
||||
"_shadowOffset": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 2,
|
||||
"y": 2
|
||||
},
|
||||
"_shadowBlur": 2,
|
||||
"_id": "93TKfIU91MsYoPl+MbLJLU"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Hint",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 14
|
||||
},
|
||||
{
|
||||
"__id__": 15
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": -60,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "edGCK/J5pJMo3QjOFjHMU8"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 13
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 900,
|
||||
"height": 60
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "0amqxP4mdHTJm3HJPkzYdS"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 13
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_customMaterial": null,
|
||||
"_srcBlendFactor": 2,
|
||||
"_dstBlendFactor": 4,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 174,
|
||||
"g": 196,
|
||||
"b": 182,
|
||||
"a": 255
|
||||
},
|
||||
"_string": "loading",
|
||||
"_horizontalAlign": 1,
|
||||
"_verticalAlign": 1,
|
||||
"_actualFontSize": 36,
|
||||
"_fontSize": 36,
|
||||
"_fontFamily": "Arial",
|
||||
"_lineHeight": 45,
|
||||
"_overflow": 2,
|
||||
"_enableWrapText": false,
|
||||
"_font": null,
|
||||
"_isSystemFontUsed": true,
|
||||
"_spacingX": 0,
|
||||
"_isItalic": false,
|
||||
"_isBold": false,
|
||||
"_isUnderline": false,
|
||||
"_underlineHeight": 2,
|
||||
"_cacheMode": 0,
|
||||
"_enableOutline": false,
|
||||
"_outlineColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 255
|
||||
},
|
||||
"_outlineWidth": 2,
|
||||
"_enableShadow": false,
|
||||
"_shadowColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 255
|
||||
},
|
||||
"_shadowOffset": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 2,
|
||||
"y": 2
|
||||
},
|
||||
"_shadowBlur": 2,
|
||||
"_id": "be14O3yOdDardPtTt2bDrD"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 1280,
|
||||
"height": 720
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "9429lRu9BL6rByObLUXHia"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Canvas",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_cameraComponent": {
|
||||
"__id__": 5
|
||||
},
|
||||
"_alignCanvasWithScreen": true,
|
||||
"_id": "7crolsyK9Cs5GFmQZmc9QW"
|
||||
},
|
||||
{
|
||||
"__type__": "c91ddke4YBE9JjRtYzU2wFp",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"nextScene": "lobby",
|
||||
"_id": "dcSHYP80NEeZirk4SzzDBT"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.SceneGlobals",
|
||||
"ambient": {
|
||||
"__id__": 20
|
||||
},
|
||||
"shadows": {
|
||||
"__id__": 21
|
||||
},
|
||||
"_skybox": {
|
||||
"__id__": 22
|
||||
},
|
||||
"fog": {
|
||||
"__id__": 23
|
||||
},
|
||||
"octree": {
|
||||
"__id__": 24
|
||||
},
|
||||
"skin": {
|
||||
"__id__": 25
|
||||
},
|
||||
"lightProbeInfo": {
|
||||
"__id__": 26
|
||||
},
|
||||
"postSettings": {
|
||||
"__id__": 27
|
||||
},
|
||||
"bakedWithStationaryMainLight": false,
|
||||
"bakedWithHighpLightmap": false
|
||||
},
|
||||
{
|
||||
"__type__": "cc.AmbientInfo",
|
||||
"_skyColorHDR": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.2,
|
||||
"y": 0.5,
|
||||
"z": 0.8,
|
||||
"w": 0.520833125
|
||||
},
|
||||
"_skyColor": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.2,
|
||||
"y": 0.5,
|
||||
"z": 0.8,
|
||||
"w": 0.520833125
|
||||
},
|
||||
"_skyIllumHDR": 20000,
|
||||
"_skyIllum": 20000,
|
||||
"_groundAlbedoHDR": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.2,
|
||||
"y": 0.2,
|
||||
"z": 0.2,
|
||||
"w": 1
|
||||
},
|
||||
"_groundAlbedo": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.2,
|
||||
"y": 0.2,
|
||||
"z": 0.2,
|
||||
"w": 1
|
||||
},
|
||||
"_skyColorLDR": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.452588,
|
||||
"y": 0.607642,
|
||||
"z": 0.755699,
|
||||
"w": 0
|
||||
},
|
||||
"_skyIllumLDR": 0.8,
|
||||
"_groundAlbedoLDR": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.618555,
|
||||
"y": 0.577848,
|
||||
"z": 0.544564,
|
||||
"w": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"__type__": "cc.ShadowsInfo",
|
||||
"_enabled": false,
|
||||
"_type": 0,
|
||||
"_normal": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 1,
|
||||
"z": 0
|
||||
},
|
||||
"_distance": 0,
|
||||
"_planeBias": 1,
|
||||
"_shadowColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 76,
|
||||
"g": 76,
|
||||
"b": 76,
|
||||
"a": 255
|
||||
},
|
||||
"_maxReceived": 4,
|
||||
"_size": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 1024,
|
||||
"y": 1024
|
||||
}
|
||||
},
|
||||
{
|
||||
"__type__": "cc.SkyboxInfo",
|
||||
"_envLightingType": 0,
|
||||
"_envmapHDR": {
|
||||
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
|
||||
"__expectedType__": "cc.TextureCube"
|
||||
},
|
||||
"_envmap": {
|
||||
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
|
||||
"__expectedType__": "cc.TextureCube"
|
||||
},
|
||||
"_envmapLDR": {
|
||||
"__uuid__": "6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0",
|
||||
"__expectedType__": "cc.TextureCube"
|
||||
},
|
||||
"_diffuseMapHDR": null,
|
||||
"_diffuseMapLDR": null,
|
||||
"_enabled": false,
|
||||
"_useHDR": true,
|
||||
"_editableMaterial": null,
|
||||
"_reflectionHDR": null,
|
||||
"_reflectionLDR": null,
|
||||
"_rotationAngle": 0
|
||||
},
|
||||
{
|
||||
"__type__": "cc.FogInfo",
|
||||
"_type": 0,
|
||||
"_fogColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 200,
|
||||
"g": 200,
|
||||
"b": 200,
|
||||
"a": 255
|
||||
},
|
||||
"_enabled": false,
|
||||
"_fogDensity": 0.3,
|
||||
"_fogStart": 0.5,
|
||||
"_fogEnd": 300,
|
||||
"_fogAtten": 5,
|
||||
"_fogTop": 1.5,
|
||||
"_fogRange": 1.2,
|
||||
"_accurate": false
|
||||
},
|
||||
{
|
||||
"__type__": "cc.OctreeInfo",
|
||||
"_enabled": false,
|
||||
"_minPos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -1024,
|
||||
"y": -1024,
|
||||
"z": -1024
|
||||
},
|
||||
"_maxPos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1024,
|
||||
"y": 1024,
|
||||
"z": 1024
|
||||
},
|
||||
"_depth": 8
|
||||
},
|
||||
{
|
||||
"__type__": "cc.SkinInfo",
|
||||
"_enabled": true,
|
||||
"_blurRadius": 0.01,
|
||||
"_sssIntensity": 3
|
||||
},
|
||||
{
|
||||
"__type__": "cc.LightProbeInfo",
|
||||
"_giScale": 1,
|
||||
"_giSamples": 1024,
|
||||
"_bounces": 2,
|
||||
"_reduceRinging": 0,
|
||||
"_showProbe": true,
|
||||
"_showWireframe": true,
|
||||
"_showConvex": false,
|
||||
"_data": null,
|
||||
"_lightProbeSphereVolume": 1
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PostSettingsInfo",
|
||||
"_toneMappingType": 0
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"ver":"1.1.50","importer":"scene","imported":true,"uuid":"1aee5910-40c3-4b99-bc8d-e477cf0e25ed","files":[".json"],"subMetas":{},"userData":{}}
|
||||
@@ -0,0 +1 @@
|
||||
{"ver":"1.1.50","importer":"scene","imported":true,"uuid":"220f80f8-6216-4ec2-b63b-72365bcbfde9","files":[".json"],"subMetas":{},"userData":{}}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CocosStorage } from '../modules/save/CocosStorage';
|
||||
import { SaveManager } from '../modules/save/SaveManager';
|
||||
import { StatsSection } from '../modules/save/StatsSection';
|
||||
|
||||
/**
|
||||
* The application's services, created once by the loader scene.
|
||||
*
|
||||
* Cocos components are built by the engine, so there is nowhere to inject
|
||||
* dependencies through a constructor; a small explicit locator is the honest
|
||||
* alternative to every component reaching for its own singleton. Everything it
|
||||
* holds is engine-independent apart from the storage adapter, so a test can
|
||||
* build one over `MemoryStorage` and drive the whole thing headlessly.
|
||||
*/
|
||||
export class App {
|
||||
private static _instance: App | null = null;
|
||||
|
||||
readonly save: SaveManager;
|
||||
readonly stats: StatsSection;
|
||||
|
||||
private constructor(save: SaveManager, stats: StatsSection) {
|
||||
this.save = save;
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
/** Builds the services and loads saved progress. Called once, from `Bootstrap`. */
|
||||
static create(): App {
|
||||
if (App._instance) {
|
||||
return App._instance;
|
||||
}
|
||||
|
||||
const stats = new StatsSection();
|
||||
const save = new SaveManager(new CocosStorage());
|
||||
save.register(stats);
|
||||
save.load();
|
||||
|
||||
App._instance = new App(save, stats);
|
||||
return App._instance;
|
||||
}
|
||||
|
||||
static get instance(): App {
|
||||
if (!App._instance) {
|
||||
throw new Error('App.create() has not run — is the loader scene the start scene?');
|
||||
}
|
||||
return App._instance;
|
||||
}
|
||||
|
||||
static get exists(): boolean {
|
||||
return App._instance !== null;
|
||||
}
|
||||
|
||||
/** Records the result of a match and writes it out immediately. */
|
||||
recordMatch(outcome: 'win' | 'loss' | 'draw'): void {
|
||||
this.stats.recordResult(outcome);
|
||||
this.save.save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "982035ad-66a6-428b-bba3-4fe31ed7a095",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { _decorator, Component, director, game, resources, SpriteFrame } from 'cc';
|
||||
import { App } from './App';
|
||||
import { CARDS_DIR } from '../ui/game/CardLibrary';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* The loader scene, per ARCHITECTURE.md: bring the modules up, then hand over
|
||||
* to the lobby. Everything that must exist before any gameplay runs belongs
|
||||
* here, and nowhere else.
|
||||
*/
|
||||
@ccclass('Bootstrap')
|
||||
export class Bootstrap extends Component {
|
||||
@property({ tooltip: 'Scene to open once the modules are ready.' })
|
||||
nextScene = 'lobby';
|
||||
|
||||
start(): void {
|
||||
// A phone that throttles to 30 fps makes the card animations stutter;
|
||||
// asking for 60 up front is cheap and the engine falls back on its own.
|
||||
game.frameRate = 60;
|
||||
|
||||
App.create();
|
||||
|
||||
// Warm the card art here rather than on the first deal: thirty-seven
|
||||
// sprites are the only sizeable load in the game, and a loader scene is
|
||||
// exactly where a player expects to wait for it.
|
||||
resources.loadDir(CARDS_DIR, SpriteFrame, (error) => {
|
||||
if (error) {
|
||||
console.error('[bootstrap] could not preload the card art', error);
|
||||
}
|
||||
this._openLobby();
|
||||
});
|
||||
}
|
||||
|
||||
private _openLobby(): void {
|
||||
director.loadScene(this.nextScene, (error) => {
|
||||
if (error) {
|
||||
console.error(`[bootstrap] could not open '${this.nextScene}'`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "c91dd91e-e180-44f4-98d1-b58cd4db0169",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Card } from '../cards/Card';
|
||||
import { Action, Actions } from './Action';
|
||||
import { GameState, Phase, PlayerId } from './GameState';
|
||||
import { beats, currentActor, legalActions, unbeatenSlots } from './DurakRules';
|
||||
|
||||
/**
|
||||
* What tapping a card means for a player right now, or null if that card cannot
|
||||
* be played.
|
||||
*
|
||||
* This lives in the core rather than in the game screen so it can be tested
|
||||
* without the engine: it is the one piece of interface logic that can silently
|
||||
* turn a legal move into a rejected tap, and it is far too easy to get wrong by
|
||||
* eye. The screen just calls it and submits whatever comes back.
|
||||
*
|
||||
* One tap is the whole interaction. When defending, the attack to beat is worked
|
||||
* out here instead of being chosen separately — the serialised rules leave only
|
||||
* one unanswered attack at a time, and where several somehow exist the lowest
|
||||
* slot this card can beat is taken.
|
||||
*/
|
||||
export function intentFor(state: GameState, player: PlayerId, card: Card): Action | null {
|
||||
if (currentActor(state) !== player) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (state.phase === Phase.Defence) {
|
||||
const slot = unbeatenSlots(state)
|
||||
.find((i) => beats(card, state.table[i].attack, state.trumpSuit));
|
||||
return slot === undefined ? null : Actions.defend(player, card, slot);
|
||||
}
|
||||
|
||||
const wanted = Actions.attack(player, card);
|
||||
const legal = legalActions(state, player)
|
||||
.some((a) => a.type === wanted.type && (a as { card?: Card }).card === card);
|
||||
return legal ? wanted : null;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b1aa773a-2d2c-43f5-980a-d6e2ffb829fd",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { _decorator, Component, Node } from 'cc';
|
||||
import { App } from '../app/App';
|
||||
import { LobbyScreen } from './lobby/LobbyScreen';
|
||||
import { GameScreen, MatchOutcome } from './game/GameScreen';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* Owns the two screens the lobby scene contains and moves between them.
|
||||
*
|
||||
* ARCHITECTURE.md puts the main game UI in the lobby scene, so a match is a
|
||||
* change of visible screen rather than a scene load: no reload cost between
|
||||
* games, and no way for a half-finished match to leak into a new one as long
|
||||
* as `GameScreen.teardown` is honest about clearing up.
|
||||
*/
|
||||
@ccclass('MainFlow')
|
||||
export class MainFlow extends Component {
|
||||
@property(LobbyScreen)
|
||||
lobby: LobbyScreen | null = null;
|
||||
|
||||
@property(GameScreen)
|
||||
game: GameScreen | null = null;
|
||||
|
||||
@property(Node)
|
||||
lobbyRoot: Node | null = null;
|
||||
|
||||
@property(Node)
|
||||
gameRoot: Node | null = null;
|
||||
|
||||
start(): void {
|
||||
if (this.lobby) {
|
||||
this.lobby.onStartRequested = () => this.startMatch();
|
||||
}
|
||||
if (this.game) {
|
||||
this.game.onFinished = (outcome) => this.finishMatch(outcome);
|
||||
this.game.onQuitRequested = () => this.abandonMatch();
|
||||
}
|
||||
this.showLobby();
|
||||
}
|
||||
|
||||
private showLobby(): void {
|
||||
setActive(this.gameRoot, false);
|
||||
setActive(this.lobbyRoot, true);
|
||||
this.lobby?.refresh();
|
||||
}
|
||||
|
||||
private startMatch(): void {
|
||||
setActive(this.lobbyRoot, false);
|
||||
setActive(this.gameRoot, true);
|
||||
this.game?.startNewMatch();
|
||||
}
|
||||
|
||||
private finishMatch(outcome: MatchOutcome): void {
|
||||
if (App.exists) {
|
||||
App.instance.recordMatch(outcome);
|
||||
}
|
||||
this.game?.teardown();
|
||||
this.showLobby();
|
||||
}
|
||||
|
||||
/** Leaving early still ends the match — and it counts as a loss. */
|
||||
private abandonMatch(): void {
|
||||
this.finishMatch('loss');
|
||||
}
|
||||
}
|
||||
|
||||
function setActive(node: Node | null, active: boolean): void {
|
||||
if (node) {
|
||||
node.active = active;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "0b757fc9-4bb2-4007-9b13-b618d0655b6e",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Every piece of text the player reads, in one place.
|
||||
*
|
||||
* Localisation is out of scope for iteration 1 (see docs/PLAN.md), but keeping
|
||||
* the strings together means adding a language later is a change to this file
|
||||
* and nothing else — rather than a hunt through scenes and components.
|
||||
*/
|
||||
export const Strings = {
|
||||
appTitle: 'Fool',
|
||||
appSubtitle: 'a game of durak',
|
||||
|
||||
newGame: 'New Game',
|
||||
quit: 'Leave',
|
||||
playAgain: 'Play Again',
|
||||
backToLobby: 'Back',
|
||||
|
||||
statsTitle: 'Your record',
|
||||
statsPlayed: 'Played',
|
||||
statsWon: 'Won',
|
||||
statsLost: 'Lost',
|
||||
statsStreak: 'Streak',
|
||||
|
||||
take: 'Take',
|
||||
done: 'Done',
|
||||
|
||||
yourMove: 'Your move',
|
||||
yourDefence: 'Beat it, or take',
|
||||
addMore: 'Add cards, or say done',
|
||||
opponentThinking: 'Opponent is thinking',
|
||||
opponentTook: 'Opponent takes the cards',
|
||||
youTook: 'You take the cards',
|
||||
|
||||
trumpIs: 'Trump',
|
||||
deckLeft: 'Deck',
|
||||
opponentHand: 'Opponent',
|
||||
|
||||
wonTitle: 'You win',
|
||||
lostTitle: 'You are the fool',
|
||||
drawTitle: 'A draw',
|
||||
wonBody: 'Your opponent was left holding the cards.',
|
||||
lostBody: 'You were left holding the cards.',
|
||||
drawBody: 'You both ran out at the same time.',
|
||||
|
||||
cannotPlayThat: 'You cannot play that card',
|
||||
} as const;
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "672ddd76-ded7-498e-97b1-df2d7d30f662",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Color } from 'cc';
|
||||
|
||||
/**
|
||||
* Shared visual constants. Anything a layout or a colour is tuned by lives here
|
||||
* rather than being typed into each scene, so a change lands everywhere at once
|
||||
* and the scene files stay free of magic numbers.
|
||||
*
|
||||
* Lengths are in design units — the screen is always exactly 1080 wide, see
|
||||
* docs/DECISIONS.md.
|
||||
*/
|
||||
export const Layout = {
|
||||
designWidth: 1080,
|
||||
designHeight: 1920,
|
||||
|
||||
/** Card sprite size, matching what scripts/build-art.mjs produces. */
|
||||
cardWidth: 220,
|
||||
cardHeight: 320,
|
||||
|
||||
/** Cards on the table are drawn smaller so six pairs fit comfortably. */
|
||||
tableCardScale: 0.62,
|
||||
/** The opponent's hand is smaller again — it is only ever card backs. */
|
||||
opponentCardScale: 0.5,
|
||||
|
||||
/** Horizontal step between neighbouring cards in the player's hand. */
|
||||
handStep: 128,
|
||||
/** Never let a hand grow wider than this; the step shrinks instead. */
|
||||
handMaxWidth: 940,
|
||||
/** How far the tapped card lifts out of the hand. */
|
||||
handLift: 48,
|
||||
|
||||
/** Offset of a beating card from the card it beats. */
|
||||
beatOffset: { x: 26, y: -34 },
|
||||
|
||||
/** Comfortable minimum for anything tappable, per docs/UI.md. */
|
||||
minTapSize: 120,
|
||||
|
||||
screenMargin: 40,
|
||||
|
||||
/**
|
||||
* Where things sit on the table, in canvas coordinates (origin at the
|
||||
* centre, y up). Vertical positions stay well inside the 1080×1920 design
|
||||
* area so that a 4:3 screen — which crops the top and bottom — still shows
|
||||
* every one of them. See docs/DECISIONS.md.
|
||||
*/
|
||||
region: {
|
||||
opponentHandY: 600,
|
||||
opponentLabelY: 770,
|
||||
deck: { x: -390, y: 150 },
|
||||
trump: { x: -300, y: 120 },
|
||||
discard: { x: 430, y: 150 },
|
||||
/** Centre of the first table row; the second row sits a row-height below. */
|
||||
tableTopY: -20,
|
||||
tableRowStep: 235,
|
||||
tableColumnStep: 180,
|
||||
messageY: 330,
|
||||
actionBarY: -430,
|
||||
playerHandY: -700,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const Palette = {
|
||||
gold: new Color(244, 194, 69, 255),
|
||||
cream: new Color(247, 240, 222, 255),
|
||||
dim: new Color(174, 196, 182, 255),
|
||||
good: new Color(126, 217, 140, 255),
|
||||
bad: new Color(232, 116, 104, 255),
|
||||
shade: new Color(0, 0, 0, 170),
|
||||
} as const;
|
||||
|
||||
/** How long the standard card movements take, in seconds. */
|
||||
export const Timing = {
|
||||
deal: 0.28,
|
||||
dealStagger: 0.06,
|
||||
play: 0.22,
|
||||
collect: 0.3,
|
||||
flip: 0.16,
|
||||
/** Pause before the bot acts, so its moves can be followed. */
|
||||
botThinking: 0.55,
|
||||
/** Pause after a turn resolves, before the next one starts. */
|
||||
turnBreak: 0.4,
|
||||
} as const;
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "3ba498f9-9dcb-48dc-bb1d-1de46602c43a",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { _decorator, Component, resources, SpriteFrame } from 'cc';
|
||||
import { Card } from '../../core/cards/Card';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** Folder under `assets/resources` holding the 36 faces and the back. */
|
||||
export const CARDS_DIR = 'cards';
|
||||
const BACK_NAME = 'card_back';
|
||||
|
||||
/**
|
||||
* Turns a `Card` into the sprite frame that draws it.
|
||||
*
|
||||
* The frames are loaded as a folder out of the `resources` bundle rather than
|
||||
* being referenced one by one in the scene: thirty-seven inspector slots would
|
||||
* be miserable to maintain, and an auto-atlas (`.pac`) is only a build-time
|
||||
* packing instruction — it is not something that can be loaded and asked for a
|
||||
* frame by name at runtime. The `.pac` beside the images still does its job,
|
||||
* packing all thirty-seven into one texture so the table costs one draw call.
|
||||
*
|
||||
* The only contract between the rules and the art is `Card.assetName`.
|
||||
*/
|
||||
@ccclass('CardLibrary')
|
||||
export class CardLibrary extends Component {
|
||||
@property({ tooltip: 'Folder under assets/resources holding the card sprites.' })
|
||||
directory = CARDS_DIR;
|
||||
|
||||
private readonly _frames = new Map<string, SpriteFrame>();
|
||||
private readonly _missingReported = new Set<string>();
|
||||
private _ready = false;
|
||||
private _waiting: (() => void)[] = [];
|
||||
|
||||
onLoad(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
get ready(): boolean {
|
||||
return this._ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the folder. Bootstrap preloads the same folder before the lobby
|
||||
* opens, so in practice this resolves from cache within a frame.
|
||||
*/
|
||||
load(): void {
|
||||
if (this._ready) {
|
||||
return;
|
||||
}
|
||||
resources.loadDir(this.directory, SpriteFrame, (error, frames) => {
|
||||
if (error) {
|
||||
console.error(`[cards] could not load '${this.directory}'`, error);
|
||||
return;
|
||||
}
|
||||
for (const frame of frames) {
|
||||
this._frames.set(frame.name, frame);
|
||||
}
|
||||
this._ready = true;
|
||||
const waiting = this._waiting;
|
||||
this._waiting = [];
|
||||
for (const resolve of waiting) resolve();
|
||||
});
|
||||
}
|
||||
|
||||
/** Runs `done` once the frames are available — immediately if they already are. */
|
||||
whenReady(done: () => void): void {
|
||||
if (this._ready) {
|
||||
done();
|
||||
} else {
|
||||
this._waiting.push(done);
|
||||
}
|
||||
}
|
||||
|
||||
frameFor(card: Card): SpriteFrame | null {
|
||||
return this._frame(card.assetName);
|
||||
}
|
||||
|
||||
get backFrame(): SpriteFrame | null {
|
||||
return this._frame(BACK_NAME);
|
||||
}
|
||||
|
||||
private _frame(name: string): SpriteFrame | null {
|
||||
const frame = this._frames.get(name) ?? null;
|
||||
if (!frame && this._ready && !this._missingReported.has(name)) {
|
||||
this._missingReported.add(name);
|
||||
console.error(`[cards] '${name}' is missing — run 'npm run build:art'`);
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "4e2c42cf-866c-4ba0-aa4f-78c256d28a0f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||