Cơ chế đổi skin của bàn chơi
"Đổi màu" trong game là đổi skin (skinId). Core đổi id ngay khi đặt khối; view đổi hình chậm hơn một nhịp để khớp hiệu ứng nâng cấp.
1. Cùng một bàn, skin khác nhau
Mỗi skin có thư mục Art/feature/skin_<id>/skins/ với board.png, bg.png và cube_1..7.png. Dưới đây là ảnh thật của ba skin.








board 885x885 · cube 106x106








board 885x885 · cube 106x106








board 894x894 · có thêm crown.png 94x70, shadow.png, fonts/, effects/
Khối nhỏ bên dưới mỗi bàn là cube_1..7. Mọi cube cao 106 px, bằng kCubeStdPx, nên một hệ số kCell/106 = 120/106 khớp ô của bàn.
2. Luồng đổi, theo thứ tự chạy
executeSkinChange (endless_game_skin.cpp): curSkinId = nextRandomSkinId, rồi rút sẵn skin kế. Cứ kSkinNeedNum = 25 tiến độ thì đổi. Pool mặc định là 4000..4019.skinPending_ = true. Trong refresh(), setDisplaySkin bị bỏ qua khi đang chờ (BoardPlay.cpp:1754), nên bàn vẫn hiện skin cũ trong lúc nâng cấp chạy.executePendingSkinChange (BoardPlay.cpp:2110): setDisplaySkin(curSkinId), rồi reskinAll().reskinAll() gọi refresh(), không thay ảnh từng ô, để cỡ ảnh, z chồng 3D và layout khay đúng theo skin mới. Khối đang kéo chỉ thay ảnh (reskinBlocks).refresh() gọi applySkinDecor (BoardView.cpp:521): xoá SkinBg, SkinBoard, SkinCrown cũ rồi dựng cái mới.fx:skin_change. Ván Travel phát playMilestoneLinesEffect, còn lại phát playSkinChangeEffects.3. Mỗi thành phần đổi thế nào
| Thành phần | Nguồn | Cách đặt |
|---|---|---|
| Ảnh khối | cube_1..7.png theo icons[] trong skins.json | blockId kẹp vào 1..7. Skin 1000 trả chuỗi rỗng, dùng ic_block. |
| Nền màn | bg.png | Kéo giãn kín màn, không giữ tỉ lệ (gốc gán SpriteFrame vào Widget 4 cạnh = 0). Ẩn WoodBgL/R. Ảnh dải 20x1707 hay 960x1707 đều xử lý như nhau. |
| Bàn | board.png | Khung, lưới và ô gộp một ảnh. Phóng 120/106, tức 885 thành khoảng 1002 px. Khi có, ẩn chessboard2, GridBorder, gezi, 64 QiCell. boardHoleOffset dò độ lệch pha lưới rồi dời ảnh. |
| Vương miện | crown.png (chỉ một số skin) | Ẩn Crown mặc định, neo đáy ảnh mới vào đáy vương miện cũ. |
| Âm xoá dòng | sound_clear.wav | Nạp sẵn bằng AudioEngine::preload để lần phát đầu không chờ đọc file. |
| Âm đặt khối | putAudioKey trong skins.json | Đọc lúc đặt khối (BoardFx.cpp:1550). |
| Màu hiệu ứng xoá | matchColors[k], ba màu RGBA mỗi khối | Skin không khai thì trích màu từ chính cube của skin (DEC-238, giả định). |
| Font số điểm | skinfont score / highscore | Dựng lại chữ số khi skin đổi. |
Mọi file decor đi qua decorPath(skinId, name) (SkinCatalog.cpp:136). File không tồn tại thì trả chuỗi rỗng và phần đó quay về hình mặc định: qipannew + QiPanxin cho bàn, nền gỗ cho màn.
4. Tổng hợp sprite liên quan
Hai nhóm: sprite đổi theo skin (đọc từ Art/feature/skin_<id>/skins/) và sprite cố định (nằm ngoài thư mục skin, dùng làm mặc định hoặc dự phòng). Ảnh là file thật trong Content/res.
Đổi theo skin

cube_1..7.png106x106. Ảnh khối, 7 ảnh mỗi skin (91 skin có đủ 7 ảnh)

board.png885x885 (skin 2011: 894). Gộp khung + lưới + ô; 89 skin có

bg.pngSkin 1000: dải 20x1707. Skin 2011: 960x1707. Kéo giãn kín màn; 98 skin có

bg.png (2011, thu nhỏ)960x1707 gốc

crown.png94x70. Thay vương miện, chỉ 32 skin có

fonts/score.fnt + .pngFont số điểm hiện tại. Còn highscore và combo

effects/xc_011_tex.pngAtlas DragonBones của hiệu ứng xoá dòng (gameplay_eliminate, xc_<id>)
Cố định, và mặc định khi skin thiếu file

ic_block.png120x120. Khối của skin 1000, và ảnh dự phòng khi skin thiếu cube

base_ic_collection_block.png120x120. Nền đào của ô gem Travel (id ≥ 100) ở skin mặc định

icon_block_big.png120x120. Khối vạn năng (id 998), vừa đúng 1 ô

BlockStyle/Default/shadow.png120x120. Bóng khối trong khay, lệch phải-xuống 20 px

icon_crown_0.png66x46. Vương miện mặc định cạnh điểm cao nhất
Có trong skin nhưng chưa thấy dùng

shadow.png106x106. 51 skin có file này, nhưng tôi không thấy code nào nạp nó
| Sprite | Số skin có (trong 100 thư mục) | Nơi nạp |
|---|---|---|
bg.png | 98 | applySkinDecor, BoardView.cpp:521 |
cube_1..7.png | 91 | makeBlock qua skinIconPath |
board.png | 89 | applySkinDecor |
shadow.png | 51 | chưa thấy nơi nạp (bóng khay dùng BlockStyle/Default/shadow.png) |
crown.png | 32 | applySkinDecor |
fonts/*.fnt | chưa đếm | skinFontNumber: score, highscore, combo, jiafen |
effects/* | chưa đếm | effectBase: gameplay_eliminate, gameplay_ready_eliminate, gameplay_eliminatePrompt |
Phép tính cỡ: ảnh khối được cubeDisplaySize ép về một ô 120 px (cube 106 px giữ tỉ lệ, neo giữa ô), còn board.png nhân 120/106. 885 × 1,132 ≈ 1002 px, đúng cỡ khung mặc định.
5. Skin chưa có trên máy
skins.json có 294 skin: 39 skin builtin (nhúng app, họ 1xxx) và 255 skin tải từ server. Trong thư mục app có 100 thư mục skin_*.setDisplaySkin gọi requestSkin: skin chưa có được chen lên đầu hàng tải.fallbackSkinId lấy 1000 + skinId mod 39 (ASM-009), chỉ đổi ảnh, id trong core giữ nguyên. Khi tải xong thì đổi ảnh thật.6. Chưa kiểm
Số liệu trên đọc từ code và từ file trong Content/res/Art/feature. Tôi chưa chạy game để xem một lần đổi skin thật. Tôi cũng chưa đọc boardHoleOffset đến cùng và chưa so từng skin với bản gốc.
Skin 1000 có board.png riêng (xanh navy trong ảnh ở mục 1). Vì vậy bộ qipannew + QiPanxin chỉ hiện khi decorPath trả rỗng. Tôi chưa xác nhận skin 1000 chạy đường nào trong game thật.