s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
link Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=verarp.colour_break&take=20
Showing code results for query:
*
(41 total matches found)
Game
game
$primary: red !default;
$primary-alt: white !default;
$switch-padding: 6px !default;
.checkbox.switch
{
cursor: pointer;
> .checkmark
{
font-size: 22px;
border: 0px solid $primary;
border-radius: 100px;
text-align: center;
justify-content: center;
align-items: center;
color: $primary-alt;
padding: $switch-padding;
padding-right: 32px;
padding-left: $switch-padding;
transition: all 0.3s ease;
background-color: rgba( $primary, 0.1 );
> .handle
{
background-color: $primary-alt;
width: 20px;
height: 20px;
border-radius: 100px;
box-shadow: 2px 2px 12px black;
}
}
&.checked
{
> .checkmark
{
background-color: $primary;
padding-left: 32px;
padding-right: $switch-padding;
}
}
&:active
{
transform: scale( 0.9 );
transform-origin: 20px 50%;
}
}
Game
game
.slidercontrol
{
flex-direction: row;
min-width: 50px;
position: relative;
flex-shrink: 0;
flex-direction: row;
cursor: pointer;
gap: 8px;
flex-grow: 1;
align-items: center;
pointer-events: all;
> .inner
{
flex-direction: column;
flex-shrink: 1;
flex-grow: 1;
min-height: 32px;
justify-content: center;
> .values
{
width: 100%;
pointer-events: none;
font-size: 14px;
color: #aaa;
> .left
{
flex-grow: 1;
}
}
> .track
{
position: relative;
background-color: #888;
height: 7px;
margin: 8px;
align-items: center;
border-radius: 4px;
> .track-active
{
background-color: #fff;
position: absolute;
height: 100%;
left: 0px;
border-radius: 4px;
}
> .thumb
{
position: relative;
background-color: #fff;
border-radius: 100px;
width: 16px;
height: 16px;
transform: translateX( -50% );
}
}
}
> .entry
{
flex-shrink: 0;
flex-grow: 0;
width: 50px;
> numberentry
{
background-color: transparent;
> .content-label
{
padding: 0 4px;
}
}
}
}
.slidercontrol .value-tooltip
{
position: absolute;
bottom: 150%;
left: -8px;
z-index: 1000;
flex-direction: column;
> .label
{
background-color: black;
padding: 8px 12px;
border-radius: 8px;
}
>.tail
{
bottom: -0px;
background-color: black;
width: 10px;
height: 10px;
transform: rotateZ(45 deg) translateX( 4px );
position: absolute;
}
}
Game
game
ColorAlphaControl
{
gap: 0.5rem;
flex-grow: 1;
pointer-events: all;
background: linear-gradient( to right, black, white );
border-radius: 4px;
padding: 2px;
height: 12px;
position: relative;
cursor: pointer;
border: 1px solid #333;
&:hover
{
border: 1px solid #08f;
}
&:active
{
border: 1px solid #fff;
}
.handle
{
top: -5px;
bottom: -5px;
aspect-ratio: 1;
border-radius: 100px;
border: 2px solid #444;
position: absolute;
background-color: white;
box-shadow: 2px 2px 16px #000a;
transform: translateX( -50% );
pointer-events: none;
}
}
Game
game
RootPanel
{
font-family: Roboto;
color: white;
}
IconPanel, i
{
font-family: Material Icons;
background-position: center;
background-size: contain;
}
.tooltip
{
background-color: rgba( black, 0.8 );
border: 1px solid rgba( #555, 0.1 );
padding: 10px;
border-radius: 4px;
color: white;
font-family: Inter;
font-weight: bold;
transition: opacity 0.09s ease;
opacity: 1;
margin: 0px;
&:intro, &:outro
{
opacity: 0;
}
}
.textentry
{
cursor: text;
align-items: center;
justify-content: center;
white-space: nowrap;
overflow: hidden;
flex-direction: row;
position: relative;
padding: 10px;
&.disabled
{
cursor: default;
}
&.is-multiline
{
align-items: flex-start;
white-space: normal;
}
.content-label,
.placeholder
{
flex-grow: 1;
}
.placeholder
{
opacity: 0.2;
pointer-events: none;
}
.iconpanel
{
position: absolute;
right: 0;
padding-right: 10px;
pointer-events: none;
&.clearbutton
{
color: #fff;
pointer-events: all;
cursor: pointer;
opacity: 0.1;
&:hover
{
opacity: 0.5;
}
&:active
{
opacity: 1;
}
}
}
&:empty
{
.clearbutton
{
display: none;
}
}
&.has-icon
{
padding-right: 30px;
}
.content-label,
.prefix-label,
.suffix-label,
.placeholder
{
overflow: hidden;
}
.content-label
{
order: 1;
}
.suffix-label
{
order: 2;
}
.prefix-label,
.suffix-label
{
flex-shrink: 0;
opacity: 0.5;
}
&.invalid
{
box-shadow: 0px 0px 1px 2px #f8356b;
}
}
.popup-panel
{
background-color: #1b202c;
flex-direction: column;
color: #8e9ec2;
transition: all 0.1s ease-out;
position: absolute;
z-index: 2000;
min-height: 20px;
min-width: 10px;
overflow: scroll;
pointer-events: all;
box-shadow: 3px 3px 64px #000;
border-radius: 4px;
&.medium
{
max-width: 300px;
}
&.center-horizontal
{
transform-origin: top center;
}
&.below-center
{
transform: translateX( -50% );
transform-origin: 0% 0%;
}
&.above-left
{
transform-origin: 0% 100%;
}
&.above-center
{
transform: translateX( -50% );
transform-origin: 0% 100%;
}
&.left
{
transform-origin: 100% 50%;
transform: translate( 00% -50% );
}
&.right
{
transform-origin: 0 50%;
transform: translate( 0% -50% );
}
&:intro
{
transform: scale( 0 );
pointer-events: none;
opacity: 0;
}
&:outro
{
opacity: 0;
transition: all 0.1s ease-in;
margin-top: 50px;
pointer-events: none;
}
&.below-stretch
{
transform-origin: 0% 0%;
&:intro
{
transform: scaleY( 0.1 );
}
}
> .canvas
{
flex-direction: column;
}
.information
{
padding: 16px;
font-size: 14px;
opacity: 0.5;
}
button
{
padding: 2px 8px;
cursor: pointer;
font-family: Poppins;
font-weight: 600;
flex-shrink: 0;
&:hover
{
background-color: #08f;
color: #fff;
}
}
}
Game
game
.cb-ui {
position: absolute;
inset: 0px;
width: 100%;
height: 100%;
font-family: "Inter", "Poppins", "Segoe UI", sans-serif;
color: #ffffff;
pointer-events: none;
}
.cb-loading {
position: absolute;
top: 24px;
left: 24px;
padding: 10px 14px;
font-size: 18px;
font-weight: 800;
color: #ffffff;
background-color: rgba(5, 8, 20, 0.88);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 8px;
}
.cb-screen {
position: absolute;
inset: 0px;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28px;
pointer-events: all;
background-color: rgba(4, 6, 16, 0.70);
}
.cb-main {
justify-content: flex-start;
gap: 0px;
padding: 24px 30px 28px;
background-color: rgba(5, 7, 18, 0.18);
}
.cb-menu-top {
width: 100%;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
}
.cb-menu-metrics {
display: flex;
flex-direction: row;
gap: 10px;
}
.cb-menu-metric {
min-width: 98px;
padding: 8px 12px;
display: flex;
flex-direction: column;
align-items: flex-start;
background-color: rgba(4, 8, 20, 0.84);
border: 1px solid rgba(255, 255, 255, 0.20);
border-radius: 8px;
box-shadow: 0px 10px 24px rgba(0, 0, 0, 0.38);
span {
font-size: 12px;
font-weight: 900;
text-transform: uppercase;
color: #aeefff;
}
b {
margin-top: -2px;
font-size: 28px;
line-height: 30px;
font-weight: 900;
color: #ffffff;
text-shadow: 0px 3px 6px rgba(0, 0, 0, 0.90);
}
}
.cb-menu-actions {
pointer-events: all;
margin-top: auto;
display: flex;
flex-direction: row;
justify-content: center;
gap: 12px;
width: auto;
padding: 12px;
background-color: rgba(4, 8, 20, 0.84);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 8px;
box-shadow: 0px 16px 42px rgba(0, 0, 0, 0.50);
.cb-btn {
width: 178px;
}
}
.cb-level-select {
pointer-events: all;
margin-top: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 12px;
width: 398px;
padding: 10px 12px;
background-color: rgba(4, 8, 20, 0.82);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
box-shadow: 0px 14px 36px rgba(0, 0, 0, 0.42);
}
.cb-level-nav {
width: 48px;
height: 46px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
line-height: 28px;
font-weight: 900;
color: #07111a;
background-color: #aeefff;
border: 2px solid rgba(255, 255, 255, 0.82);
border-radius: 8px;
box-shadow: 0px 0px 16px rgba(92, 237, 255, 0.46);
cursor: pointer;
transition: all 0.10s ease-out;
&:hover {
transform: scale(1.04);
border-color: #ffffff;
}
&:active {
transform: scale(0.97);
}
&.disabled {
color: rgba(255, 255, 255, 0.34);
background-color: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.12);
box-shadow: none;
}
}
.cb-level-select-copy {
width: 262px;
display: flex;
flex-direction: column;
align-items: center;
span {
font-size: 12px;
font-weight: 900;
text-transform: uppercase;
color: #aeefff;
text-shadow: 0px 2px 3px rgba(0, 0, 0, 1);
}
b {
margin-top: -1px;
font-size: 19px;
line-height: 22px;
font-weight: 900;
color: #ffffff;
text-align: center;
text-shadow: 0px 3px 5px rgba(0, 0, 0, 1);
}
}
.cb-title-wrap {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.cb-mode-tabs {
display: flex;
flex-direction: row;
gap: 8px;
margin: 6px 0px 2px;
padding: 5px;
background-color: rgba(4, 8, 20, 0.78);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 10px;
}
.cb-mode-tab {
pointer-events: all;
width: 150px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
font-weight: 900;
letter-spacing: 0px;
color: rgba(255, 255, 255, 0.66);
background-color: rgba(255, 255, 255, 0.05);
border: 2px solid rgba(255, 255, 255, 0.10);
border-radius: 8px;
cursor: pointer;
transition: all 0.10s ease-out;
&:hover {
color: #ffffff;
border-color: rgba(92, 237, 255, 0.55);
}
&.active {
color: #07111a;
background-color: #aeefff;
border-color: #ffffff;
box-shadow: 0px 0px 16px rgba(92, 237, 255, 0.55);
}
}
.cb-record-row {
color: #07111a;
background-color: #ffd23f;
border-radius: 6px;
span {
color: #07111a;
}
b {
color: #07111a;
}
}
.cb-title {
margin: 0px;
font-size: 62px;
line-height: 64px;
font-weight: 900;
letter-spacing: 0px;
color: #ffffff;
text-shadow:
0px 4px 0px rgba(20, 224, 255, 0.85),
0px 10px 0px rgba(199, 56, 255, 0.55),
0px 22px 42px rgba(0, 0, 0, 0.75);
}
.cb-tagline {
margin: 0px;
padding: 7px 12px;
font-size: 16px;
font-weight: 800;
color: #f4fbff;
background-color: rgba(4, 8, 20, 0.78);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.9);
}
.cb-level-brief {
max-width: 520px;
margin: 0px;
padding: 8px 12px;
font-size: 15px;
line-height: 19px;
font-weight: 800;
color: rgba(255, 255, 255, 0.88);
background-color: rgba(4, 8, 20, 0.66);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.95);
}
.cb-level-objective {
max-width: 520px;
margin: 0px;
padding: 7px 12px;
font-size: 15px;
line-height: 18px;
font-weight: 900;
color: #07111a;
background-color: #aeefff;
border: 1px solid rgba(255, 255, 255, 0.85);
border-radius: 8px;
box-shadow: 0px 0px 18px rgba(92, 237, 255, 0.38);
}
.cb-level-rail {
display: flex;
flex-direction: row;
align-items: center;
gap: 7px;
margin-top: 4px;
padding: 6px 8px;
background-color: rgba(4, 8, 20, 0.58);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
}
.cb-level-pip {
min-width: 28px;
height: 28px;
padding: 0px 7px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 900;
color: rgba(255, 255, 255, 0.54);
background-color: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 8px;
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.90);
&.complete {
color: #04120b;
background-color: rgba(66, 247, 123, 0.92);
border-color: rgba(196, 255, 214, 0.95);
text-shadow: none;
}
&.active {
color: #07111a;
background-color: #aeefff;
border-color: #ffffff;
box-shadow: 0px 0px 18px rgba(92, 237, 255, 0.80);
text-shadow: none;
}
&.locked {
color: rgba(255, 255, 255, 0.36);
background-color: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.08);
}
}
.cb-buttons {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: 320px;
}
.cb-btn {
pointer-events: all;
width: 100%;
height: 58px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-weight: 900;
letter-spacing: 0px;
color: #ffffff;
background-color: rgba(11, 16, 32, 0.90);
border: 2px solid rgba(255, 255, 255, 0.22);
border-radius: 8px;
box-shadow: 0px 12px 28px rgba(0, 0, 0, 0.40);
transition: all 0.10s ease-out;
cursor: pointer;
&:hover {
background-color: rgba(25, 36, 64, 0.96);
border-color: rgba(255, 255, 255, 0.70);
transform: scale(1.035);
}
&:active {
transform: scale(0.98);
}
}
.cb-primary {
color: #04120b;
background-color: #42f77b;
border-color: #c4ffd6;
text-shadow: none;
&:hover {
background-color: #6dff98;
border-color: #ffffff;
}
}
.cb-disabled {
color: rgba(255, 255, 255, 0.42);
background-color: rgba(255, 255, 255, 0.07);
border-color: rgba(255, 255, 255, 0.14);
box-shadow: none;
&:hover {
background-color: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.18);
transform: none;
}
}
.cb-hint {
position: absolute;
bottom: 30px;
left: 0px;
right: 0px;
align-self: center;
width: 620px;
margin: 0px auto;
padding: 11px 18px;
justify-content: center;
text-align: center;
font-size: 16px;
font-weight: 800;
color: #f4fbff;
background-color: rgba(4, 8, 20, 0.82);
border: 1px solid rgba(92, 237, 255, 0.45);
border-radius: 10px;
box-shadow: 0px 10px 28px rgba(0, 0, 0, 0.50);
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.95);
}
.cb-hint-stuck {
color: #07111a;
background-color: #aeefff;
border-color: #ffffff;
box-shadow: 0px 0px 22px rgba(92, 237, 255, 0.55);
text-shadow: none;
}
.cb-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 22px;
padding: 38px 48px;
min-width: 380px;
background-color: rgba(5, 8, 22, 0.94);
border: 2px solid rgba(92, 237, 255, 0.42);
border-radius: 8px;
box-shadow: 0px 22px 70px rgba(0, 0, 0, 0.72);
}
.cb-settings-screen {
z-index: 50;
background-color: rgba(3, 5, 14, 0.94);
}
.cb-settings-panel {
z-index: 51;
min-width: 420px;
background-color: rgba(5, 8, 22, 0.98);
}
.cb-settings-panel .cb-panel-title {
font-size: 40px;
}
.cb-setting-row {
pointer-events: all;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 24px;
width: 100%;
min-height: 52px;
padding: 10px 14px;
color: rgba(255, 255, 255, 0.82);
background-color: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
font-size: 17px;
font-weight: 900;
cursor: pointer;
b {
color: #42f77b;
font-size: 18px;
}
&:hover {
background-color: rgba(66, 247, 123, 0.16);
border-color: rgba(66, 247, 123, 0.72);
}
&.focused {
color: #ffffff;
background-color: rgba(92, 237, 255, 0.18);
border-color: rgba(92, 237, 255, 0.92);
box-shadow: 0px 0px 18px rgba(92, 237, 255, 0.42);
}
}
.cb-panel-title {
margin: 0px;
font-size: 46px;
font-weight: 900;
color: #ffffff;
text-shadow: 0px 5px 18px rgba(78, 226, 255, 0.65);
}
.cb-readout {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.cb-readout-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 24px;
width: 100%;
padding: 9px 12px;
font-size: 17px;
font-weight: 800;
color: rgba(255, 255, 255, 0.68);
background-color: rgba(255, 255, 255, 0.06);
border-radius: 6px;
b {
color: #ffffff;
font-weight: 900;
}
}
.cb-hud {
position: absolute;
top: 20px;
left: 20px;
width: 300px;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
pointer-events: none;
}
.cb-stat,
.cb-status {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
min-height: 58px;
padding: 8px 14px;
background-color: rgba(4, 7, 18, 0.92);
border: 2px solid rgba(255, 255, 255, 0.24);
border-radius: 8px;
box-shadow:
0px 10px 24px rgba(0, 0, 0, 0.45),
inset 0px 0px 18px rgba(58, 226, 255, 0.10);
}
.cb-stat-label,
.cb-status-label {
font-size: 14px;
font-weight: 900;
text-transform: uppercase;
color: #aeefff;
text-shadow: 0px 2px 3px rgba(0, 0, 0, 1);
}
.cb-stat-value {
margin-top: -2px;
font-size: 32px;
line-height: 32px;
font-weight: 900;
color: #ffffff;
text-shadow: 0px 3px 5px rgba(0, 0, 0, 1);
}
.cb-stat-subtitle {
margin-top: 2px;
font-size: 14px;
line-height: 16px;
font-weight: 900;
color: rgba(255, 255, 255, 0.82);
text-shadow: 0px 2px 4px rgba(0, 0, 0, 1);
}
.cb-status {
min-width: 0px;
}
.cb-progress {
width: 100%;
min-height: 62px;
padding: 9px 14px;
display: flex;
flex-direction: column;
justify-content: center;
background-color: rgba(4, 7, 18, 0.92);
border: 2px solid rgba(255, 255, 255, 0.24);
border-radius: 8px;
box-shadow:
0px 10px 24px rgba(0, 0, 0, 0.45),
inset 0px 0px 18px rgba(66, 247, 123, 0.10);
}
.cb-progress-copy {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 12px;
span {
font-size: 14px;
font-weight: 900;
text-transform: uppercase;
color: #aeefff;
text-shadow: 0px 2px 3px rgba(0, 0, 0, 1);
}
b {
font-size: 18px;
font-weight: 900;
color: #ffffff;
text-shadow: 0px 2px 3px rgba(0, 0, 0, 1);
}
}
.cb-progress-track {
width: 100%;
height: 10px;
margin-top: 7px;
background-color: rgba(255, 255, 255, 0.10);
border-radius: 5px;
overflow: hidden;
}
.cb-progress-fill {
height: 100%;
min-width: 3px;
background-color: #42f77b;
border-radius: 5px;
box-shadow: 0px 0px 18px rgba(66, 247, 123, 0.75);
}
.cb-objective {
box-shadow:
0px 10px 24px rgba(0, 0, 0, 0.45),
inset 0px 0px 18px rgba(92, 237, 255, 0.16);
}
.cb-objective-name {
margin-top: 2px;
font-size: 13px;
line-height: 15px;
font-weight: 900;
color: rgba(255, 255, 255, 0.86);
text-shadow: 0px 2px 3px rgba(0, 0, 0, 1);
}
.cb-objective-fill {
background-color: #aeefff;
box-shadow: 0px 0px 18px rgba(92, 237, 255, 0.82);
}
.cb-status-value {
margin-top: 2px;
font-size: 20px;
line-height: 22px;
font-weight: 900;
color: #ffffff;
text-shadow: 0px 3px 5px rgba(0, 0, 0, 1);
}
.cb-pause-btn {
position: absolute;
top: 20px;
right: 20px;
pointer-events: all;
width: 68px;
min-height: 68px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: 900;
color: #ffffff;
background-color: rgba(4, 7, 18, 0.92);
border: 2px solid rgba(255, 255, 255, 0.30);
border-radius: 8px;
box-shadow: 0px 10px 24px rgba(0, 0, 0, 0.45);
cursor: pointer;
transition: all 0.10s ease-out;
&:hover {
color: #04120b;
background-color: #42f77b;
border-color: #ffffff;
transform: scale(1.04);
}
&:active {
transform: scale(0.97);
}
}
/* -------- bug reporting -------- */
.cb-report-panel {
min-width: 520px;
}
.cb-report-intro {
margin: 0px;
max-width: 460px;
font-size: 15px;
line-height: 19px;
font-weight: 700;
text-align: center;
color: rgba(255, 255, 255, 0.82);
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.9);
}
.cb-report-input {
width: 460px;
height: 150px;
padding: 12px 14px;
font-size: 16px;
font-weight: 600;
color: #ffffff;
background-color: rgba(255, 255, 255, 0.06);
border: 2px solid rgba(92, 237, 255, 0.45);
border-radius: 8px;
&:focus {
border-color: #aeefff;
background-color: rgba(255, 255, 255, 0.10);
}
}
.cb-report-feedback {
width: 460px;
padding: 8px 12px;
font-size: 15px;
font-weight: 800;
text-align: center;
color: #07120b;
background-color: #42f77b;
border-radius: 8px;
}
.cb-manager-btn {
color: #07111a;
background-color: #ffd23f;
border-color: #ffe98a;
&:hover {
background-color: #ffe06a;
border-color: #ffffff;
}
}
.cb-manager-panel {
min-width: 620px;
max-width: 760px;
}
.cb-manager-list {
display: flex;
flex-direction: column;
gap: 10px;
width: 660px;
max-height: 460px;
overflow-y: scroll;
padding-right: 4px;
}
.cb-manager-empty {
padding: 28px;
font-size: 17px;
font-weight: 800;
text-align: center;
color: rgba(255, 255, 255, 0.6);
}
.cb-report-row {
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px 14px;
background-color: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.14);
border-left-width: 4px;
border-left-color: #ff7a59;
border-radius: 8px;
&.resolved {
border-left-color: #42f77b;
opacity: 0.7;
}
}
.cb-report-head {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
.cb-report-when {
font-size: 13px;
font-weight: 900;
color: #aeefff;
}
.cb-report-meta {
flex-grow: 1;
font-size: 13px;
font-weight: 700;
color: rgba(255, 255, 255, 0.66);
}
.cb-report-tag {
padding: 2px 8px;
font-size: 12px;
font-weight: 900;
color: #07120b;
background-color: #42f77b;
border-radius: 10px;
}
.cb-report-msg {
font-size: 16px;
font-weight: 600;
line-height: 20px;
color: #ffffff;
white-space: pre-wrap;
}
.cb-report-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 8px;
margin-top: 2px;
}
.cb-mini-btn {
pointer-events: all;
padding: 6px 14px;
font-size: 14px;
font-weight: 900;
color: #07111a;
background-color: #aeefff;
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: 6px;
cursor: pointer;
transition: all 0.10s ease-out;
&:hover {
background-color: #ffffff;
transform: scale(1.04);
}
}
.cb-mini-danger {
color: #ffffff;
background-color: rgba(255, 90, 70, 0.92);
border-color: rgba(255, 150, 130, 0.9);
&:hover {
background-color: #ff5a46;
}
}
Game
game
using Sandbox;
using System;
public static class ColourBreakProgressStore
{
const string ProgressCookieKey = "colourbreak.progress.v1";
public static ColourBreakSaveData Load()
{
ColourBreakSaveData data;
try
{
data = Game.Cookies.Get( ProgressCookieKey, new ColourBreakSaveData() ) ?? new ColourBreakSaveData();
}
catch ( Exception e )
{
Log.Warning( $"Could not load Colour Break progress: {e.Message}" );
data = new ColourBreakSaveData();
}
Normalize( data );
return data;
}
public static void Save( ColourBreakSaveData data )
{
Normalize( data );
try
{
Game.Cookies.Set( ProgressCookieKey, data );
}
catch ( Exception e )
{
Log.Warning( $"Could not save Colour Break progress: {e.Message}" );
}
}
public static int GetBestStars( ColourBreakSaveData data, int levelIndex )
{
if ( data?.BestStars == null )
return 0;
if ( levelIndex < 0 || levelIndex >= data.BestStars.Length )
return 0;
return Math.Clamp( data.BestStars[levelIndex], 0, 3 );
}
public static bool RecordEndlessScore( ColourBreakSaveData data, int score )
{
Normalize( data );
if ( score <= data.BestEndlessScore )
return false;
data.BestEndlessScore = score;
Save( data );
return true;
}
public static void RecordWin( ColourBreakSaveData data, int levelIndex, int stars, bool hasNextLevel )
{
Normalize( data );
if ( levelIndex >= 0 && levelIndex < data.BestStars.Length )
data.BestStars[levelIndex] = Math.Max( data.BestStars[levelIndex], Math.Clamp( stars, 1, 3 ) );
if ( hasNextLevel )
data.HighestUnlockedLevel = Math.Max( data.HighestUnlockedLevel, levelIndex + 1 );
Save( data );
}
static void Normalize( ColourBreakSaveData data )
{
if ( data == null )
return;
data.HighestUnlockedLevel = Math.Clamp( data.HighestUnlockedLevel, 1, ColourBreakLevels.Count );
if ( data.BestEndlessScore < 0 )
data.BestEndlessScore = 0;
if ( data.BestStars == null || data.BestStars.Length < ColourBreakLevels.Count + 1 )
{
var old = data.BestStars;
data.BestStars = new int[ColourBreakLevels.Count + 1];
if ( old != null )
{
int count = Math.Min( old.Length, data.BestStars.Length );
for ( int i = 0; i < count; i++ )
data.BestStars[i] = Math.Clamp( old[i], 0, 3 );
}
}
for ( int i = 0; i < data.BestStars.Length; i++ )
data.BestStars[i] = Math.Clamp( data.BestStars[i], 0, 3 );
}
}
Game
game
.splitcontainer
{
flex-grow: 1;
> .split-left, > .split-right
{
flex-grow: 1;
}
> .split-left
{
height: 100%;
}
> .splitter
{
flex-grow: 1;
width: 8px;
cursor: ew-resize;
&:hover
{
}
}
&.dragging > .splitter
{
}
&.vertical
{
flex-direction: column;
> .splitter
{
height: 8px;
width: 100%;
cursor: ns-resize;
}
}
}
Game
game
.navigator-body
{
&.hidden
{
display: none;
}
}
Game
game
$form-control-height: 28px !default;
@import "form/_checkbox.scss";
@import "form/_switch.scss";
@import "form/_dropdown.scss";
@import "form/_coloreditor.scss";
@import "form/_colorproperty.scss";
.form
{
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
overflow: scroll;
}
.field-group
{
flex-direction: column;
flex-shrink: 0;
}
.field-header
{
flex-shrink: 0;
}
.field
{
color: white;
font-size: 14px;
flex-shrink: 0;
flex-grow: 0;
> .label
{
flex-grow: 0;
flex-shrink: 0;
font-weight: 600;
opacity: 0.4;
width: 20%;
font-size: 13px;
}
> .control
{
flex-shrink: 0;
flex-grow: 1;
flex-direction: column;
}
}
.is-vertical > .field, .field.is-vertical
{
flex-direction: column;
> .label
{
width: auto;
height: auto;
}
}
Game
game
@import "base/_splitcontainer.scss";
@import "base/_navigator.scss";
button
{
cursor: pointer;
}
IconPanel
{
font-family: Material Icons;
}
.is-half
{
width: 50%;
}
.is-third
{
width: 33%;
}
.is-quarter
{
width: 25%;
}
button.has-subtitle
{
position: relative;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding-left: 40px; // icon space
.iconpanel
{
position: absolute;
left: 5px;
top: 0;
bottom: 0;
align-items: center;
}
.button-label
{
font-weight: bold;
}
.button-subtitle
{
font-size: 12px;
opacity: 0.5;
mix-blend-mode: lighten;
}
}
Game
game
EnumControl
{
gap: 2px;
flex-grow: 1;
}
EnumControl DropDown,
EnumControl ButtonGroup
{
border-radius: 8px;
background-color: #000a;
flex-grow: 1;
}
EnumControl DropDown
{
flex-grow: 1;
min-height: 32px;
}
EnumControl ButtonGroup
{
border-radius: 12px;
overflow: hidden;
min-height: 32px;
Button
{
flex-grow: 1;
justify-content: center;
align-items: center;
gap: 4px;
color: #aaa;
font-size: 1rem;
cursor: pointer;
.icon
{
color: #08f;
}
&:hover
{
color: #ddd;
.icon
{
color: #3af;
}
}
&:active
{
background-color: #04a;
color: white;
transform: translateX( 1px ) translateY( 1px );
.icon
{
color: #fff;
}
}
&.active
{
background-color: #08f;
color: white;
pointer-events: none;
.icon
{
color: #fff;
}
}
}
}
Game
game
public sealed class ColourBreakSaveData
{
public int HighestUnlockedLevel { get; set; } = 1;
public int[] BestStars { get; set; } = new int[ColourBreakLevels.Count + 1];
public int BestEndlessScore { get; set; }
}
Game
game
PackageFlairBar
{
position: absolute;
top: 6px;
left: 6px;
z-index: 2;
flex-direction: row;
gap: 5px;
pointer-events: none;
.flair
{
position: relative;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
pointer-events: all;
border-radius: 4px;
color: white;
border: 1px solid rgba( white, 0.1 );
outline: 1px solid #0006;
opacity: 0.7;
&:hover
{
opacity: 1;
}
icon
{
font-size: 15px;
}
}
}
Game
game
using Sandbox;
using System;
using System.Linq;
using System.Collections.Generic;
// Local persistence for bug reports, stored in a cookie like the progress store.
// No network calls — the dev reads them in-game via the hidden manager.
public static class ColourBreakBugReportStore
{
const string CookieKey = "colourbreak.bugreports.v1";
const int MaxReports = 200;
const int MaxMessageLength = 600;
public static ColourBreakBugReportLog Load()
{
try
{
return Game.Cookies.Get( CookieKey, new ColourBreakBugReportLog() ) ?? new ColourBreakBugReportLog();
}
catch ( Exception e )
{
Log.Warning( $"Could not load bug reports: {e.Message}" );
return new ColourBreakBugReportLog();
}
}
public static void Save( ColourBreakBugReportLog log )
{
if ( log == null ) return;
try
{
Game.Cookies.Set( CookieKey, log );
}
catch ( Exception e )
{
Log.Warning( $"Could not save bug reports: {e.Message}" );
}
}
public static List<ColourBreakBugReport> GetAll()
{
return Load().Reports ?? new List<ColourBreakBugReport>();
}
public static int OpenCount()
{
return GetAll().Count( r => r != null && !r.Resolved );
}
public static bool Add( ColourBreakBugReport report )
{
if ( report == null )
return false;
report.Message = (report.Message ?? "").Trim();
if ( string.IsNullOrWhiteSpace( report.Message ) )
return false;
if ( report.Message.Length > MaxMessageLength )
report.Message = report.Message.Substring( 0, MaxMessageLength );
var log = Load();
log.Reports ??= new List<ColourBreakBugReport>();
log.Reports.Insert( 0, report ); // newest first
if ( log.Reports.Count > MaxReports )
log.Reports.RemoveRange( MaxReports, log.Reports.Count - MaxReports );
Save( log );
return true;
}
public static void SetResolved( string id, bool resolved )
{
var log = Load();
var r = log.Reports?.FirstOrDefault( x => x != null && x.Id == id );
if ( r == null ) return;
r.Resolved = resolved;
Save( log );
}
public static void Delete( string id )
{
var log = Load();
log.Reports?.RemoveAll( x => x == null || x.Id == id );
Save( log );
}
public static void Clear()
{
Save( new ColourBreakBugReportLog() );
}
}
Game
game
using Sandbox;
using System;
using System.Collections.Generic;
public sealed class ColourBreakGame : Component
{
const int Empty = -1;
[Property] public int Width { get; set; } = 8;
[Property] public int Height { get; set; } = 8;
[Property] public float CellSpacing { get; set; } = 48f;
[Property] public float CandyScale { get; set; } = 0.50f;
[Property] public Vector3 BoardOrigin { get; set; } = Vector3.Zero;
[Property] public Model CandyModel { get; set; }
[Property] public int CandyTypeCount { get; set; } = 6;
[Property] public bool AutoFrameMainCamera { get; set; } = true;
[Property] public int LevelIndex { get; set; } = 1;
[Property] public int TargetScore { get; set; } = 850;
[Property] public int MoveLimit { get; set; } = 24;
[Property] public bool AudioEnabled { get; set; } = true;
[Property] public bool EffectsEnabled { get; set; } = true;
[Property] public bool CameraShakeEnabled { get; set; } = true;
[Property] public bool ColourBlindMode { get; set; }
[Property] public string StartSound { get; set; } = "sounds/colourbreak_start.sound";
[Property] public string SwapSound { get; set; } = "sounds/colourbreak_swap.sound";
[Property] public string ClearSound { get; set; } = "sounds/colourbreak_clear.sound";
[Property] public string ComboSound { get; set; } = "sounds/colourbreak_combo.sound";
[Property] public string DeniedSound { get; set; } = "sounds/colourbreak_denied.sound";
[Property] public bool MusicEnabled { get; set; } = true;
[Property] public string MusicSound { get; set; } = "sounds/colourbreak_music.sound";
[Property, Range( 0f, 1f )] public float MusicVolume { get; set; } = 0.42f;
[Property] public GameMode Mode { get; set; } = GameMode.Levels;
// Endless uses the full palette and the largest shape set available.
const int EndlessColourCount = 6;
Model _sphereModel;
Model _boxModel;
Model ModelForType( int t ) => _sphereModel ?? _boxModel;
Color PieceColor( int type ) => (ColourBlindMode ? ColourBlindPalette : Palette)[Math.Clamp( type, 0, Palette.Length - 1 )];
Vector3 PieceShape( int type ) => (ColourBlindMode ? ColourBlindShapes : Shapes)[Math.Clamp( type, 0, Shapes.Length - 1 )];
// Distinct saturated colours that read quickly under motion and bloom.
readonly Color[] Palette = new[]
{
new Color( 1.00f, 0.05f, 0.15f ), // bright red
new Color( 1.00f, 0.50f, 0.00f ), // bright orange
new Color( 1.00f, 0.95f, 0.00f ), // bright yellow
new Color( 0.15f, 0.90f, 0.15f ), // bright green
new Color( 0.05f, 0.55f, 1.00f ), // bright blue
new Color( 0.80f, 0.15f, 1.00f ), // bright purple
};
// Okabe-Ito-derived hues, pushed brighter/more saturated so they stay distinct
// under bloom. Backed up by the per-type shape symbols in ApplyColourBlindSymbol.
readonly Color[] ColourBlindPalette = new[]
{
new Color( 1.00f, 0.55f, 0.00f ), // vivid orange
new Color( 0.30f, 0.78f, 1.00f ), // sky blue
new Color( 0.00f, 0.78f, 0.50f ), // bluish green
new Color( 1.00f, 0.95f, 0.20f ), // yellow
new Color( 0.05f, 0.35f, 0.95f ), // deep blue
new Color( 0.95f, 0.40f, 0.70f ), // reddish purple / pink
};
readonly string[] ColourNames =
{
"Ruby",
"Amber",
"Gold",
"Lime",
"Azure",
"Violet",
};
readonly Vector3[] Shapes = new[]
{
new Vector3( 1.00f, 1.00f, 1.00f ),
new Vector3( 1.18f, 0.92f, 1.08f ),
new Vector3( 0.92f, 1.08f, 1.24f ),
new Vector3( 1.08f, 1.00f, 0.86f ),
new Vector3( 0.94f, 1.06f, 1.14f ),
new Vector3( 1.10f, 0.90f, 0.98f ),
new Vector3( 1.00f, 1.00f, 1.00f ),
};
readonly Vector3[] ColourBlindShapes = new[]
{
new Vector3( 0.78f, 1.00f, 1.42f ),
new Vector3( 1.42f, 0.92f, 0.78f ),
new Vector3( 1.00f, 1.00f, 1.00f ),
new Vector3( 1.28f, 0.92f, 1.28f ),
new Vector3( 0.78f, 1.08f, 0.78f ),
new Vector3( 1.48f, 0.86f, 1.08f ),
new Vector3( 1.00f, 1.00f, 1.00f ),
};
readonly Dictionary<string, bool> _prevKeys = new();
int[,] _types;
SpecialKind[,] _specials;
CandyVisual[,] _visuals;
readonly List<GameObject> _decor = new();
int _cursorX;
int _cursorY;
bool _hasSelection;
int _selectedX;
int _selectedY;
int _score;
int _moves;
int _objectiveProgress;
ColourBreakSaveData _saveData = new();
bool _prevMouseLeft;
float _controllerMoveCooldown;
bool _dragging;
int _dragStartX;
int _dragStartY;
bool _dragSwapped;
// Idle hint: after a stretch of inactivity, gently pulse a valid move so the
// player is never truly stuck staring at the board.
float _idleTime;
bool _hasHint;
int _hintAX, _hintAY, _hintBX, _hintBY;
const float HintDelay = 5.0f;
// Tracks whether the player has ever made a swap this session, so the
// how-to-play prompt can fade out once they clearly understand the controls.
bool _hasEverSwapped;
// High-level game state driving the menus
public enum GameState { Menu, Playing, Paused, Won, Lost }
public enum GameMode { Levels, Endless }
public GameState CurrentState { get; private set; } = GameState.Menu;
public bool IsEndless => Mode == GameMode.Endless;
enum SpecialKind { None, RowBlast, ColumnBlast, Bomb }
// Exposed for the UI / HUD
public int Score => _score;
public int Moves => _moves;
public int MovesLeft => Math.Max( 0, MoveLimit - _moves );
public int GoalScore => TargetScore;
public int Level => LevelIndex;
public int ColourCount => CandyTypeCount;
ColourBreakLevelDefinition CurrentLevelDefinition => ColourBreakLevels.Get( LevelIndex );
public string LevelName => CurrentLevelDefinition.Name;
public string LevelBrief => CurrentLevelDefinition.Brief;
public int LevelCount => ColourBreakLevels.Count;
public bool HasNextLevel => LevelIndex < ColourBreakLevels.Count;
public int HighestUnlockedLevel => Math.Clamp( _saveData?.HighestUnlockedLevel ?? 1, 1, LevelCount );
public int BestStars => ColourBreakProgressStore.GetBestStars( _saveData, LevelIndex );
public int TotalStars
{
get
{
int total = 0;
for ( int i = 1; i <= LevelCount; i++ )
total += ColourBreakProgressStore.GetBestStars( _saveData, i );
return total;
}
}
public int MaxStars => LevelCount * 3;
public int EndlessBest => Math.Max( 0, _saveData?.BestEndlessScore ?? 0 );
public bool EndlessRecordSet { get; private set; }
public bool SelectedLevelLocked => LevelIndex > HighestUnlockedLevel;
public bool CanPlaySelectedLevel => !SelectedLevelLocked;
public bool CanSelectPreviousLevel => CurrentState == GameState.Menu && LevelIndex > 1;
public bool CanSelectNextLevel => CurrentState == GameState.Menu && LevelIndex < LevelCount;
public bool HasSecondaryObjective => CurrentLevelDefinition.ObjectiveKind != ColourBreakObjectiveKind.Score && CurrentLevelDefinition.ObjectiveTarget > 0;
public int ObjectiveProgress => HasSecondaryObjective ? Math.Min( _objectiveProgress, CurrentLevelDefinition.ObjectiveTarget ) : 0;
public int ObjectiveTarget => HasSecondaryObjective ? CurrentLevelDefinition.ObjectiveTarget : 0;
public string ObjectiveText => ObjectiveLabel();
public string ObjectiveProgressText => HasSecondaryObjective
? $"{ObjectiveProgress} / {ObjectiveTarget}"
: "Score target";
public bool ObjectiveComplete => !HasSecondaryObjective || _objectiveProgress >= CurrentLevelDefinition.ObjectiveTarget;
public int CompletionStars
{
get
{
if ( CurrentState != GameState.Won )
return 0;
int stars = 1;
if ( MovesLeft >= Math.Max( 3, MoveLimit / 4 ) )
stars++;
if ( _score >= MathF.Ceiling( TargetScore * 1.25f ) )
stars++;
return Math.Clamp( stars, 1, 3 );
}
}
public string CompletionRank => CompletionStars switch
{
3 => "Perfect Break",
2 => "Clean Break",
1 => "Level Clear",
_ => "Keep Going"
};
public int ComboCount => _comboCount;
public bool HasLegalMove => HasPossibleMove();
public bool MusicIsPlaying => _music.IsValid() && _music.IsPlaying;
public int ActiveSpecialCount => CountSpecials();
// Drives the one-time how-to-play prompt and the "stuck?" hint banner in the HUD.
public bool ShowHowToPlay => CurrentState == GameState.Playing && !_hasEverSwapped;
public bool HintActive => _hasHint && CurrentState == GameState.Playing;
public string StatusText => _phase switch
{
Phase.SwapForward => "Swapping",
Phase.SwapBack => "Try another move",
Phase.Clearing => _comboCount > 1 ? $"Combo x{_comboCount}" : "Colour break",
Phase.Collapsing => "Dropping",
Phase.Refilling => "Refilling",
Phase.Pause => _comboCount > 1 ? $"Chain x{_comboCount}" : "Scoring",
_ when _hasSelection => "Pick a neighbour",
_ => CurrentState switch
{
GameState.Won => "Level complete",
GameState.Lost => IsEndless ? "No moves left" : "Out of moves",
GameState.Playing => IsEndless ? "Keep the chain alive" : "Find a colour chain",
_ => "Ready"
}
};
// Resolve / animation state machine
enum Phase { Idle, SwapForward, ValidateAndClear, Clearing, Collapsing, Refilling, Pause, SwapBack }
Phase _phase = Phase.Idle;
float _phaseTimer;
int _swapAX, _swapAY, _swapBX, _swapBY;
int _comboCount;
// Camera punch
CameraComponent _activeCamera;
float _baseFov = 60f;
float _fovPunch;
float _shakeAmount;
Vector3 _baseCameraPos;
// Orbiting accent lights
GameObject _orbitLightL;
GameObject _orbitLightR;
// Color-shifting tray
ModelRenderer _trayInnerRenderer;
Color _trayBaseColor = new Color( 0.62f, 0.70f, 0.92f );
const float SwapDuration = 0.18f;
const float ClearDuration = 0.28f;
const float FallSpeed = 1100f; // units per second
const float CascadePause = 0.05f;
sealed class CandyVisual
{
public GameObject Go;
public ModelRenderer Renderer;
public GameObject Shine;
public ModelRenderer ShineRenderer;
public GameObject Rim;
public ModelRenderer RimRenderer;
public GameObject Accent;
public ModelRenderer AccentRenderer;
public GameObject SymbolA;
public ModelRenderer SymbolARenderer;
public GameObject SymbolB;
public ModelRenderer SymbolBRenderer;
public GameObject Face;
public ModelRenderer FaceRenderer;
public GameObject Halo;
public ModelRenderer HaloRenderer;
public GameObject Guide;
public ModelRenderer GuideRenderer;
public GameObject SpecialBadge;
public ModelRenderer SpecialBadgeRenderer;
public int Type;
public SpecialKind Special;
public Vector3 Anchor; // resting/target world position
public Vector3 Velocity; // for falling
public float BobPhase;
public float ClearTimer; // > 0 while playing destroy anim
public bool Clearing;
public bool Spawning; // newly spawned, scaling up from 0
public float SpawnTimer;
public float SwapT; // 0..1 progress for swap tween
public Vector3 SwapFrom;
public Vector3 SwapTo;
public bool Swapping;
}
sealed class Burst
{
public GameObject Go;
public ModelRenderer Renderer;
public Vector3 Velocity;
public float Life;
public float MaxLife;
public Color BaseTint;
public float StartScale;
}
readonly List<Burst> _bursts = new();
// Particle pool. Bursts/fire spawn dozens of short-lived renderer objects per
// clear; recycling them instead of new/Destroy each time avoids GC churn.
readonly Stack<GameObject> _burstPool = new();
const int MaxPooledBursts = 256;
GameObject RentBurstObject( string name )
{
GameObject go;
if ( _burstPool.Count > 0 )
{
go = _burstPool.Pop();
go.Name = name;
go.Enabled = true;
}
else
{
go = new GameObject( true, name );
go.Flags = GameObjectFlags.NotNetworked;
go.AddComponent<ModelRenderer>();
}
return go;
}
void ReturnBurstObject( GameObject go )
{
if ( go == null || !go.IsValid() ) return;
if ( _burstPool.Count >= MaxPooledBursts )
{
go.Destroy();
return;
}
go.Enabled = false;
go.SetParent( null );
_burstPool.Push( go );
}
sealed class Shockwave
{
public GameObject Go;
public ModelRenderer Renderer;
public Color Tint;
public float Life;
public float MaxLife;
public float Radius;
}
readonly List<Shockwave> _shockwaves = new();
float _audioCooldown;
SoundHandle _music;
// -------------------- lifecycle --------------------
protected override void OnStart()
{
base.OnStart();
_saveData = ColourBreakProgressStore.Load();
ApplyLevelSettings();
_boxModel = Model.Load( "models/dev/box.vmdl" );
_sphereModel = Model.Load( "models/dev/sphere.vmdl" );
CandyModel ??= _boxModel;
EnsureSceneLighting();
BuildEnvironment();
BuildFreshBoard();
if ( AutoFrameMainCamera )
FrameMainCameraToBoard();
Mouse.Visibility = MouseVisibility.Visible;
// Start at the main menu; the board sits in the background as a live backdrop.
CurrentState = GameState.Menu;
Log.Info( "Colour Break ready. Press Play to start. Drag a colour piece onto an adjacent one to swap. R resets, Esc pauses." );
}
protected override void OnUpdate()
{
HandlePauseHotkeys();
UpdateMusic();
// Paused: freeze the board entirely until the player resumes.
if ( CurrentState == GameState.Paused )
return;
AnimateCandies();
AnimateBursts();
AnimateShockwaves();
UpdatePhase();
UpdateAmbience();
UpdateCameraPunch();
_audioCooldown = MathF.Max( 0f, _audioCooldown - Time.Delta );
_controllerMoveCooldown = MathF.Max( 0f, _controllerMoveCooldown - Time.Delta );
// Only accept gameplay input while actually playing.
if ( CurrentState == GameState.Playing && _phase == Phase.Idle )
{
HandleMouse();
HandleInput();
}
UpdateHint();
ApplyHighlights();
}
// -------------------- game state / menu API --------------------
void ApplyLevelSettings()
{
LevelIndex = Math.Clamp( LevelIndex, 1, ColourBreakLevels.Count );
var level = CurrentLevelDefinition;
TargetScore = level.TargetScore;
MoveLimit = level.MoveLimit;
CandyTypeCount = Math.Clamp( level.ColourCount, 4, Math.Min( Palette.Length, Shapes.Length ) );
}
public int GetBestStars( int levelIndex )
{
return ColourBreakProgressStore.GetBestStars( _saveData, levelIndex );
}
void SaveWinProgress()
{
ColourBreakProgressStore.RecordWin( _saveData, LevelIndex, CompletionStars, HasNextLevel );
SubmitLeaderboardStats();
}
// Cumulative stats feed s&box leaderboards once the published package defines
// them. Guarded so it is a harmless no-op in the editor / offline.
void SubmitLeaderboardStats()
{
try
{
Sandbox.Services.Stats.Increment( "levels-cleared", 1 );
Sandbox.Services.Stats.Increment( "stars-earned", CompletionStars );
Sandbox.Services.Stats.Increment( "total-score", _score );
}
catch ( Exception e )
{
Log.Info( $"Leaderboard stats unavailable: {e.Message}" );
}
}
string ObjectiveLabel()
{
var level = CurrentLevelDefinition;
return level.ObjectiveKind switch
{
ColourBreakObjectiveKind.ClearPieces => $"Break {level.ObjectiveTarget} pieces",
ColourBreakObjectiveKind.ClearColour => $"Break {level.ObjectiveTarget} {ColourName( level.ObjectiveType )}",
ColourBreakObjectiveKind.MakeSpecials => $"Create {level.ObjectiveTarget} specials",
ColourBreakObjectiveKind.TriggerSpecials => $"Trigger {level.ObjectiveTarget} specials",
ColourBreakObjectiveKind.ChainCombos => $"Chain {level.ObjectiveTarget} cascades",
_ => $"Score {TargetScore}"
};
}
string ColourName( int type )
{
if ( type >= 0 && type < ColourNames.Length )
return ColourNames[type];
return "colour";
}
void ApplyEndlessSettings()
{
CandyTypeCount = Math.Clamp( EndlessColourCount, 4, Math.Min( Palette.Length, Shapes.Length ) );
TargetScore = int.MaxValue; // no score win condition in endless
MoveLimit = int.MaxValue; // no move limit in endless
}
// Switch the selected mode from the main menu and refresh the backdrop board.
public void SetMode( GameMode mode )
{
if ( CurrentState != GameState.Menu || Mode == mode )
return;
Mode = mode;
if ( IsEndless ) ApplyEndlessSettings();
else ApplyLevelSettings();
BuildFreshBoard();
}
public void SelectLevelsMode() => SetMode( GameMode.Levels );
public void SelectEndlessMode() => SetMode( GameMode.Endless );
public void StartNewGame()
{
EndlessRecordSet = false;
if ( IsEndless )
{
ApplyEndlessSettings();
}
else
{
ApplyLevelSettings();
if ( SelectedLevelLocked )
{
LevelIndex = HighestUnlockedLevel;
ApplyLevelSettings();
}
}
BuildFreshBoard();
CurrentState = GameState.Playing;
_comboCount = 0;
PlayUiSound( StartSound, 0.85f, 1f );
SpawnFireColumn( BoardOrigin + new Vector3( 0f, -14f, 0f ), 0.80f );
SpawnShockwave( BoardOrigin, new Color( 1f, 0.42f, 0.08f ), 0.55f, 110f );
PunchCamera( 3.5f, 2.0f );
Mouse.Visibility = MouseVisibility.Visible;
}
public void RestartGame() => StartNewGame();
public void StartNextLevel()
{
if ( HasNextLevel && LevelIndex < HighestUnlockedLevel )
LevelIndex++;
StartNewGame();
}
public void SelectPreviousLevel()
{
if ( CurrentState != GameState.Menu )
return;
LevelIndex = Math.Max( 1, LevelIndex - 1 );
ApplyLevelSettings();
BuildFreshBoard();
}
public void SelectNextLevel()
{
if ( CurrentState != GameState.Menu )
return;
LevelIndex = Math.Min( LevelCount, LevelIndex + 1 );
ApplyLevelSettings();
BuildFreshBoard();
}
public void PauseGame()
{
if ( CurrentState == GameState.Playing )
CurrentState = GameState.Paused;
}
public void ResumeGame()
{
if ( CurrentState == GameState.Paused )
CurrentState = GameState.Playing;
}
public void QuitToMenu()
{
if ( IsEndless ) ApplyEndlessSettings();
else ApplyLevelSettings();
BuildFreshBoard(); // rebuild a fresh backdrop and reset stats
CurrentState = GameState.Menu;
}
public void ToggleAudio() => AudioEnabled = !AudioEnabled;
public void ToggleMusic() => MusicEnabled = !MusicEnabled;
public void ToggleEffects() => EffectsEnabled = !EffectsEnabled;
public void ToggleCameraShake() => CameraShakeEnabled = !CameraShakeEnabled;
public void ToggleColourBlindMode()
{
ColourBlindMode = !ColourBlindMode;
RefreshAllPieceVisuals();
}
// -------------------- bug reporting --------------------
// Reports are stored locally; only this Steam ID sees the in-game manager.
const long DevSteamId = 76561198271608270L;
public bool IsDeveloper => LocalSteamId() == DevSteamId;
public int OpenBugReportCount => ColourBreakBugReportStore.OpenCount();
static long LocalSteamId()
{
try { return Connection.Local?.SteamId.Value ?? 0L; }
catch { return 0L; }
}
string LocalReporterName()
{
try
{
var c = Connection.Local;
if ( c != null && !string.IsNullOrWhiteSpace( c.DisplayName ) )
return c.DisplayName;
}
catch { }
return "Player";
}
string BugContext()
{
if ( IsEndless )
return $"Endless - Score {_score}, Moves {_moves}";
return $"Level {LevelIndex} ({LevelName}) - {CurrentState}";
}
public bool SubmitBugReport( string message )
{
if ( string.IsNullOrWhiteSpace( message ) )
return false;
var report = new ColourBreakBugReport
{
Message = message,
Reporter = LocalReporterName(),
SteamId = LocalSteamId(),
Context = BugContext(),
Version = "1.0",
};
bool ok = ColourBreakBugReportStore.Add( report );
if ( ok )
PlayUiSound( ClearSound, 0.6f, 1.1f );
return ok;
}
public List<ColourBreakBugReport> GetBugReports() => ColourBreakBugReportStore.GetAll();
public void ResolveBugReport( string id ) => ColourBreakBugReportStore.SetResolved( id, true );
public void ReopenBugReport( string id ) => ColourBreakBugReportStore.SetResolved( id, false );
public void DeleteBugReport( string id ) => ColourBreakBugReportStore.Delete( id );
public void ClearBugReports() => ColourBreakBugReportStore.Clear();
void HandlePauseHotkeys()
{
// Esc toggles pause while in a game; ignored on the main menu.
if ( CurrentState == GameState.Menu || CurrentState == GameState.Won || CurrentState == GameState.Lost ) return;
if ( Pressed( "Escape" ) || ActionPressed( "menu", "pause" ) )
{
if ( CurrentState == GameState.Playing ) PauseGame();
else if ( CurrentState == GameState.Paused ) ResumeGame();
}
}
// -------------------- environment --------------------
void BuildEnvironment()
{
CleanupDecor();
float boardW = Width * CellSpacing;
float boardH = Height * CellSpacing;
float edge = 18f;
float depth = 28f;
AddStageBackdrop( boardW, boardH, edge, depth );
_decor.Add( CreateBlock( "Tray",
BoardOrigin + new Vector3( 0f, depth * 0.5f + 8f, 0f ),
new Vector3( boardW + edge * 6.8f, depth, boardH + edge * 6.8f ),
new Color( 0.025f, 0.028f, 0.050f ) ) );
var trayInner = CreateBlock( "TrayInner",
BoardOrigin + new Vector3( 0f, depth * 0.5f + 2f, 0f ),
new Vector3( boardW + edge * 2.6f, depth - 4f, boardH + edge * 2.6f ),
new Color( 0.055f, 0.065f, 0.105f ) );
_trayInnerRenderer = trayInner.GetComponent<ModelRenderer>();
_trayBaseColor = new Color( 0.055f, 0.065f, 0.105f );
for ( int y = 0; y < Height; y++ )
{
for ( int x = 0; x < Width; x++ )
{
float cellX = (x - (Width - 1) * 0.5f) * CellSpacing;
float cellZ = (y - (Height - 1) * 0.5f) * CellSpacing;
float alt = ((x + y) & 1) == 0 ? 0.00f : 0.018f;
var tile = CreateBlock( $"WallTile_{x}_{y}",
BoardOrigin + new Vector3( cellX, depth * 0.5f - 2f, cellZ ),
new Vector3( CellSpacing * 0.86f, 3.5f, CellSpacing * 0.86f ),
new Color( 0.075f + alt, 0.085f + alt, 0.135f + alt ) );
tile.WorldRotation = Rotation.FromYaw( ((x + y) % 3 - 1) * 1.4f );
}
}
for ( int x = 0; x <= Width; x++ )
{
float gridX = (x - Width * 0.5f) * CellSpacing + CellSpacing * 0.5f;
_decor.Add( CreateBlock( $"GridV_{x}",
BoardOrigin + new Vector3( gridX, depth * 0.5f - 5f, 0f ),
new Vector3( 1.2f, 1.8f, boardH + edge * 0.6f ),
new Color( 0.025f, 0.22f, 0.34f ) ) );
}
for ( int y = 0; y <= Height; y++ )
{
float gridZ = (y - Height * 0.5f) * CellSpacing + CellSpacing * 0.5f;
_decor.Add( CreateBlock( $"GridH_{y}",
BoardOrigin + new Vector3( 0f, depth * 0.5f - 5f, gridZ ),
new Vector3( boardW + edge * 0.6f, 1.8f, 1.2f ),
new Color( 0.025f, 0.22f, 0.34f ) ) );
}
var frameTint = new Color( 0.19f, 0.10f, 0.31f );
_decor.Add( CreateBlock( "FrameTop",
BoardOrigin + new Vector3( 0f, -4f, boardH * 0.5f + edge * 1.0f ),
new Vector3( boardW + edge * 5.2f, edge * 1.8f, edge * 1.8f ), frameTint ) );
_decor.Add( CreateBlock( "FrameBottom",
BoardOrigin + new Vector3( 0f, -4f, -boardH * 0.5f - edge * 1.0f ),
new Vector3( boardW + edge * 5.2f, edge * 1.8f, edge * 1.8f ), frameTint ) );
_decor.Add( CreateBlock( "FrameLeft",
BoardOrigin + new Vector3( -boardW * 0.5f - edge * 1.0f, -4f, 0f ),
new Vector3( edge * 1.8f, edge * 1.8f, boardH + edge * 5.2f ), frameTint ) );
_decor.Add( CreateBlock( "FrameRight",
BoardOrigin + new Vector3( boardW * 0.5f + edge * 1.0f, -4f, 0f ),
new Vector3( edge * 1.8f, edge * 1.8f, boardH + edge * 5.2f ), frameTint ) );
var railTint = new Color( 0.05f, 0.78f, 1f );
_decor.Add( CreateBlock( "GlowRailTop",
BoardOrigin + new Vector3( 0f, -12f, boardH * 0.5f + edge * 2.2f ),
new Vector3( boardW + edge * 4.6f, edge * 0.28f, edge * 0.30f ), railTint ) );
_decor.Add( CreateBlock( "GlowRailBottom",
BoardOrigin + new Vector3( 0f, -12f, -boardH * 0.5f - edge * 2.2f ),
new Vector3( boardW + edge * 4.6f, edge * 0.28f, edge * 0.30f ), new Color( 1f, 0.28f, 0.05f ) ) );
_decor.Add( CreateBlock( "GlowRailLeft",
BoardOrigin + new Vector3( -boardW * 0.5f - edge * 2.2f, -12f, 0f ),
new Vector3( edge * 0.30f, edge * 0.28f, boardH + edge * 4.6f ), new Color( 0.82f, 0.16f, 1f ) ) );
_decor.Add( CreateBlock( "GlowRailRight",
BoardOrigin + new Vector3( boardW * 0.5f + edge * 2.2f, -12f, 0f ),
new Vector3( edge * 0.30f, edge * 0.28f, boardH + edge * 4.6f ), new Color( 1f, 0.65f, 0.08f ) ) );
AddPointLight( "AccentL", BoardOrigin + new Vector3( -boardW * 0.72f, -boardH * 0.50f, 118f ), new Color( 1f, 0.35f, 0.95f ), 1000f );
AddPointLight( "AccentR", BoardOrigin + new Vector3( boardW * 0.72f, -boardH * 0.50f, 118f ), new Color( 0.25f, 0.92f, 1f ), 1000f );
AddPointLight( "AccentTop", BoardOrigin + new Vector3( 0f, -boardH * 0.62f, boardH * 0.82f ), new Color( 1f, 0.85f, 0.48f ), 1200f );
_orbitLightL = AddPointLightReturn( "OrbitL", BoardOrigin, new Color( 1f, 0.28f, 0.08f ), 760f );
_orbitLightR = AddPointLightReturn( "OrbitR", BoardOrigin, new Color( 0.25f, 0.95f, 1f ), 760f );
_decor.Add( CreateBlock( "Floor",
BoardOrigin + new Vector3( 0f, depth + 66f, -boardH * 0.5f - edge * 5.0f ),
new Vector3( boardW * 1.78f, 7f, edge * 7.0f ),
new Color( 0.018f, 0.020f, 0.035f ) ) );
AddBottomCabinetLighting( boardW, boardH, edge );
AddSideCabinetControls( -1, boardW, boardH, edge, new Color( 0.82f, 0.16f, 1f ), new Color( 1f, 0.55f, 0.08f ) );
AddSideCabinetControls( 1, boardW, boardH, edge, new Color( 1f, 0.55f, 0.08f ), new Color( 0.05f, 0.78f, 1f ) );
}
GameObject CreateBlock( string name, Vector3 pos, Vector3 scale, Color tint )
{
var go = new GameObject( true, name );
go.Flags = GameObjectFlags.NotNetworked;
go.WorldPosition = pos;
go.WorldScale = scale / 50f; // box.vmdl is ~50 units
var mr = go.AddComponent<ModelRenderer>();
mr.Model = _boxModel ?? CandyModel;
mr.Tint = tint;
_decor.Add( go );
return go;
}
void AddStageBackdrop( float boardW, float boardH, float edge, float depth )
{
float backY = depth + 142f;
var backTint = new Color( 0.030f, 0.028f, 0.056f );
CreateBlock( "StageBackdrop",
BoardOrigin + new Vector3( 0f, backY, 0f ),
new Vector3( boardW * 3.25f, 10f, boardH * 2.45f ),
backTint );
CreateBlock( "StageBackdropCore",
BoardOrigin + new Vector3( 0f, backY - 6f, 0f ),
new Vector3( boardW * 1.55f, 6f, boardH * 1.55f ),
new Color( 0.050f, 0.055f, 0.090f ) );
CreateBlock( "StageLeftWing",
BoardOrigin + new Vector3( -boardW * 1.08f, backY - 3f, 0f ),
new Vector3( edge * 3.2f, 8f, boardH * 2.28f ),
new Color( 0.052f, 0.032f, 0.085f ) );
CreateBlock( "StageRightWing",
BoardOrigin + new Vector3( boardW * 1.08f, backY - 3f, 0f ),
new Vector3( edge * 3.2f, 8f, boardH * 2.28f ),
new Color( 0.030f, 0.060f, 0.088f ) );
CreateBlock( "StageTopLine",
BoardOrigin + new Vector3( 0f, backY - 8f, boardH * 0.78f ),
new Vector3( boardW * 2.25f, 4f, edge * 0.18f ),
new Color( 0.05f, 0.78f, 1f ) );
CreateBlock( "StageBottomLine",
BoardOrigin + new Vector3( 0f, backY - 8f, -boardH * 0.78f ),
new Vector3( boardW * 2.05f, 4f, edge * 0.18f ),
new Color( 1f, 0.34f, 0.06f ) );
AddPointLight( "StageWashLeft",
BoardOrigin + new Vector3( -boardW * 0.95f, depth - 36f, boardH * 0.12f ),
new Color( 0.75f, 0.18f, 1f ),
900f );
AddPointLight( "StageWashRight",
BoardOrigin + new Vector3( boardW * 0.95f, depth - 36f, -boardH * 0.08f ),
new Color( 0.08f, 0.78f, 1f ),
900f );
}
void AddSideCabinetControls( int side, float boardW, float boardH, float edge, Color ledTint, Color buttonTint )
{
string label = side < 0 ? "Left" : "Right";
float x = side * (boardW * 0.5f + edge * 1.28f);
float y = -18.5f;
_decor.Add( CreateBlock( $"Cabinet{label}Inset",
BoardOrigin + new Vector3( x, y + 0.4f, 0f ),
new Vector3( edge * 0.58f, edge * 0.52f, boardH * 0.74f ),
new Color( 0.075f, 0.045f, 0.115f ) ) );
_decor.Add( CreateBlock( $"Cabinet{label}LedRail",
BoardOrigin + new Vector3( x - side * edge * 0.18f, y - 4.2f, 0f ),
new Vector3( edge * 0.12f, edge * 0.12f, boardH * 0.70f ),
ledTint ) );
_decor.Add( CreateBlock( $"Cabinet{label}GoldTrimTop",
BoardOrigin + new Vector3( x - side * edge * 0.02f, y - 3.4f, boardH * 0.35f ),
new Vector3( edge * 0.42f, edge * 0.18f, edge * 0.12f ),
new Color( 1f, 0.72f, 0.22f ) ) );
_decor.Add( CreateBlock( $"Cabinet{label}GoldTrimBottom",
BoardOrigin + new Vector3( x - side * edge * 0.02f, y - 3.4f, -boardH * 0.35f ),
new Vector3( edge * 0.42f, edge * 0.18f, edge * 0.12f ),
new Color( 1f, 0.72f, 0.22f ) ) );
AddSideButton( $"Cabinet{label}ButtonTop", BoardOrigin + new Vector3( x - side * edge * 0.03f, y - 5.0f, boardH * 0.22f ), buttonTint, 1.0f );
AddSideButton( $"Cabinet{label}ButtonBottom", BoardOrigin + new Vector3( x - side * edge * 0.03f, y - 5.0f, -boardH * 0.18f ), Color.Lerp( buttonTint, Color.White, 0.24f ), 0.82f );
AddPointLight( $"Cabinet{label}SideGlow",
BoardOrigin + new Vector3( x - side * edge * 0.25f, y - 35f, 0f ),
ledTint,
430f );
}
void AddBottomCabinetLighting( float boardW, float boardH, float edge )
{
float z = -boardH * 0.5f - edge * 1.12f;
float y = -18.5f;
_decor.Add( CreateBlock( "CabinetBottomInset",
BoardOrigin + new Vector3( 0f, y + 0.3f, z ),
new Vector3( boardW + edge * 2.8f, edge * 0.48f, edge * 0.58f ),
new Color( 0.070f, 0.042f, 0.105f ) ) );
_decor.Add( CreateBlock( "CabinetBottomGoldRail",
BoardOrigin + new Vector3( 0f, y - 3.6f, z - edge * 0.24f ),
new Vector3( boardW + edge * 2.4f, edge * 0.14f, edge * 0.14f ),
new Color( 1f, 0.68f, 0.18f ) ) );
_decor.Add( CreateBlock( "CabinetBottomGlowRail",
BoardOrigin + new Vector3( 0f, y - 4.8f, z + edge * 0.18f ),
new Vector3( boardW + edge * 1.7f, edge * 0.10f, edge * 0.12f ),
new Color( 1f, 0.33f, 0.05f ) ) );
for ( int i = 0; i < 5; i++ )
{
float x = MathX.Lerp( -boardW * 0.42f, boardW * 0.42f, i / 4f );
var tint = i % 2 == 0 ? new Color( 1f, 0.48f, 0.08f ) : new Color( 0.05f, 0.78f, 1f );
AddFooterLamp( $"CabinetBottomLamp_{i}", BoardOrigin + new Vector3( x, y - 5.4f, z + edge * 0.02f ), tint );
}
AddPointLight( "CabinetBottomGlow",
BoardOrigin + new Vector3( 0f, y - 38f, z + edge * 0.10f ),
new Color( 1f, 0.42f, 0.08f ),
520f );
}
void AddFooterLamp( string name, Vector3 pos, Color tint )
{
var rim = new GameObject( true, $"{name}Rim" );
rim.Flags = GameObjectFlags.NotNetworked;
rim.WorldPosition = pos;
rim.WorldScale = new Vector3( 0.105f, 0.024f, 0.105f );
var rimRenderer = rim.AddComponent<ModelRenderer>();
rimRenderer.Model = _sphereModel ?? _boxModel ?? CandyModel;
rimRenderer.Tint = new Color( 0.40f, 0.42f, 0.50f );
_decor.Add( rim );
var face = new GameObject( true, $"{name}Face" );
face.Flags = GameObjectFlags.NotNetworked;
face.WorldPosition = pos + new Vector3( 0f, -1.4f, 0f );
face.WorldScale = new Vector3( 0.070f, 0.018f, 0.070f );
var faceRenderer = face.AddComponent<ModelRenderer>();
faceRenderer.Model = _sphereModel ?? _boxModel ?? CandyModel;
faceRenderer.Tint = tint;
_decor.Add( face );
}
void AddSideButton( string name, Vector3 pos, Color tint, float size )
{
var rim = new GameObject( true, $"{name}Rim" );
rim.Flags = GameObjectFlags.NotNetworked;
rim.WorldPosition = pos;
rim.WorldScale = new Vector3( 0.26f * size, 0.070f * size, 0.26f * size );
var rimRenderer = rim.AddComponent<ModelRenderer>();
rimRenderer.Model = _sphereModel ?? _boxModel ?? CandyModel;
rimRenderer.Tint = new Color( 0.46f, 0.48f, 0.58f );
_decor.Add( rim );
var face = new GameObject( true, $"{name}Face" );
face.Flags = GameObjectFlags.NotNetworked;
face.WorldPosition = pos + new Vector3( 0f, -2.4f, 0f );
face.WorldScale = new Vector3( 0.18f * size, 0.050f * size, 0.18f * size );
var faceRenderer = face.AddComponent<ModelRenderer>();
faceRenderer.Model = _sphereModel ?? _boxModel ?? CandyModel;
faceRenderer.Tint = tint;
_decor.Add( face );
var shine = new GameObject( true, $"{name}Shine" );
shine.Flags = GameObjectFlags.NotNetworked;
shine.WorldPosition = pos + new Vector3( -3.4f * size, -4.2f, 4.2f * size );
shine.WorldScale = new Vector3( 0.055f * size, 0.018f * size, 0.032f * size );
var shineRenderer = shine.AddComponent<ModelRenderer>();
shineRenderer.Model = _sphereModel ?? _boxModel ?? CandyModel;
shineRenderer.Tint = new Color( 1f, 1f, 1f, 0.82f );
_decor.Add( shine );
}
void AddPointLight( string name, Vector3 pos, Color color, float radius )
{
var go = new GameObject( true, name );
go.Flags = GameObjectFlags.NotNetworked;
go.WorldPosition = pos;
var l = go.AddComponent<PointLight>();
l.LightColor = color;
l.Radius = radius;
_decor.Add( go );
}
GameObject AddPointLightReturn( string name, Vector3 pos, Color color, float radius )
{
var go = new GameObject( true, name );
go.Flags = GameObjectFlags.NotNetworked;
go.WorldPosition = pos;
var l = go.AddComponent<PointLight>();
l.LightColor = color;
l.Radius = radius;
_decor.Add( go );
return go;
}
// -------------------- board --------------------
void BuildFreshBoard()
{
ClearBoardObjects();
_types = new int[Width, Height];
_specials = new SpecialKind[Width, Height];
_visuals = new CandyVisual[Width, Height];
int attempts = 0;
do
{
ClearBoardObjects();
_types = new int[Width, Height];
_specials = new SpecialKind[Width, Height];
_visuals = new CandyVisual[Width, Height];
for ( int y = 0; y < Height; y++ )
{
for ( int x = 0; x < Width; x++ )
{
int type = PickTypeAvoidingImmediateMatch( x, y );
SpawnCandy( x, y, type, instant: true );
}
}
}
while ( !HasPossibleMove() && ++attempts < 20 );
_cursorX = Width / 2;
_cursorY = Height / 2;
_hasSelection = false;
_score = 0;
_moves = 0;
_objectiveProgress = 0;
_phase = Phase.Idle;
_idleTime = 0f;
_hasHint = false;
}
void SpawnCandy( int x, int y, int type, bool instant )
{
_types[x, y] = type;
var anchor = CellToWorld( x, y );
var go = new GameObject( true, $"Piece_{x}_{y}" );
go.Flags = GameObjectFlags.NotNetworked;
go.WorldRotation = Rotation.FromYaw( Game.Random.Float( -7f, 7f ) );
var renderer = go.AddComponent<ModelRenderer>();
renderer.Model = ModelForType( type );
var pieceColor = PieceColor( type );
renderer.Tint = Color.Lerp( pieceColor, Color.Black, 0.28f );
var rim = AddPiecePart( go, $"PieceRim_{x}_{y}", _sphereModel ?? _boxModel,
new Vector3( 0f, -4.0f, 0f ), new Vector3( 1.08f, 0.24f, 1.08f ), Color.Lerp( pieceColor, Color.Black, 0.42f ) );
var face = AddPiecePart( go, $"PieceFace_{x}_{y}", _sphereModel ?? _boxModel,
new Vector3( 0f, -9.5f, 0.6f ), new Vector3( 0.76f, 0.14f, 0.76f ), Color.Lerp( pieceColor, Color.White, 0.20f ) );
var halo = AddPiecePart( go, $"PieceHalo_{x}_{y}", _sphereModel ?? _boxModel,
new Vector3( 0f, -5.4f, 0f ), Vector3.Zero, new Color( 1f, 0.95f, 0.22f, 0.82f ) );
var guide = AddPiecePart( go, $"PieceGuide_{x}_{y}", _sphereModel ?? _boxModel,
new Vector3( 0f, -5.0f, 0f ), Vector3.Zero, new Color( 0.22f, 0.92f, 1f, 0.62f ) );
var accent = AddPiecePart( go, $"PieceAccent_{x}_{y}", _boxModel ?? _sphereModel,
new Vector3( 0f, -13.0f, 7.0f ), new Vector3( 0.34f, 0.034f, 0.048f + (type % 3) * 0.020f ), Color.Lerp( pieceColor, Color.White, 0.70f ) );
accent.LocalRotation = Rotation.FromYaw( type * 28f );
var symbolA = AddPiecePart( go, $"PieceSymbolA_{x}_{y}", _boxModel ?? _sphereModel,
new Vector3( 0f, -18.0f, 0.0f ), Vector3.Zero, new Color( 0.02f, 0.025f, 0.035f, 1f ) );
var symbolB = AddPiecePart( go, $"PieceSymbolB_{x}_{y}", _boxModel ?? _sphereModel,
new Vector3( 0f, -18.4f, 0.0f ), Vector3.Zero, new Color( 1f, 1f, 1f, 1f ) );
var shine = AddPiecePart( go, $"PieceShine_{x}_{y}", _sphereModel ?? _boxModel,
new Vector3( -8.5f, -14.0f, 9.0f ), new Vector3( 0.16f, 0.026f, 0.080f ), new Color( 1f, 1f, 1f, 0.86f ) );
var badge = AddPiecePart( go, $"PieceSpecial_{x}_{y}", _boxModel ?? _sphereModel,
new Vector3( 0f, -15.0f, -8.0f ), Vector3.Zero, new Color( 1f, 0.82f, 0.18f, 1f ) );
var v = new CandyVisual
{
Go = go,
Renderer = renderer,
Shine = shine,
ShineRenderer = shine.GetComponent<ModelRenderer>(),
Rim = rim,
RimRenderer = rim.GetComponent<ModelRenderer>(),
Face = face,
FaceRenderer = face.GetComponent<ModelRenderer>(),
Halo = halo,
HaloRenderer = halo.GetComponent<ModelRenderer>(),
Guide = guide,
GuideRenderer = guide.GetComponent<ModelRenderer>(),
Accent = accent,
AccentRenderer = accent.GetComponent<ModelRenderer>(),
SymbolA = symbolA,
SymbolARenderer = symbolA.GetComponent<ModelRenderer>(),
SymbolB = symbolB,
SymbolBRenderer = symbolB.GetComponent<ModelRenderer>(),
SpecialBadge = badge,
SpecialBadgeRenderer = badge.GetComponent<ModelRenderer>(),
Type = type,
Special = SpecialKind.None,
Anchor = anchor,
BobPhase = Game.Random.Float( 0f, MathF.PI * 2f ),
Spawning = !instant,
SpawnTimer = instant ? 0f : 0.25f,
};
if ( instant )
{
go.WorldPosition = anchor;
go.WorldScale = Vector3.One * CandyScale * PieceShape( type );
}
else
{
go.WorldPosition = anchor + new Vector3( 0f, 0f, Height * CellSpacing * 0.6f + Game.Random.Float( 0f, 60f ) );
go.WorldScale = Vector3.One * CandyScale * PieceShape( type ) * 0.4f;
}
_visuals[x, y] = v;
ApplyPieceVisualStyle( v );
}
GameObject AddPiecePart( GameObject parent, string name, Model model, Vector3 localPosition, Vector3 localScale, Color tint )
{
var part = new GameObject( true, name );
part.Flags = GameObjectFlags.NotNetworked;
part.SetParent( parent, false );
part.LocalPosition = localPosition;
part.LocalScale = localScale;
var mr = part.AddComponent<ModelRenderer>();
mr.Model = model;
mr.Tint = tint;
return part;
}
void ApplyPieceVisualStyle( CandyVisual v )
{
if ( v == null ) return;
var color = PieceColor( v.Type );
if ( v.Renderer != null ) v.Renderer.Tint = Color.Lerp( color, Color.Black, 0.28f );
if ( v.RimRenderer != null ) v.RimRenderer.Tint = Color.Lerp( color, Color.Black, 0.42f );
if ( v.FaceRenderer != null ) v.FaceRenderer.Tint = Color.Lerp( color, Color.White, 0.20f );
if ( v.AccentRenderer != null ) v.AccentRenderer.Tint = Color.Lerp( color, Color.White, 0.70f );
ApplyColourBlindSymbol( v );
}
void ApplyColourBlindSymbol( CandyVisual v )
{
if ( v?.SymbolA == null || v.SymbolB == null )
return;
if ( !ColourBlindMode )
{
v.SymbolA.LocalScale = Vector3.Zero;
v.SymbolB.LocalScale = Vector3.Zero;
return;
}
v.SymbolA.LocalRotation = Rotation.Identity;
v.SymbolB.LocalRotation = Rotation.Identity;
v.SymbolA.LocalScale = Vector3.Zero;
v.SymbolB.LocalScale = Vector3.Zero;
if ( v.SymbolARenderer != null ) v.SymbolARenderer.Tint = new Color( 0.02f, 0.025f, 0.035f, 1f );
if ( v.SymbolBRenderer != null ) v.SymbolBRenderer.Tint = new Color( 1f, 1f, 1f, 1f );
switch ( v.Type )
{
case 0:
v.SymbolB.LocalScale = new Vector3( 0.19f, 0.036f, 0.70f );
v.SymbolA.LocalScale = new Vector3( 0.11f, 0.040f, 0.58f );
break;
case 1:
v.SymbolB.LocalScale = new Vector3( 0.70f, 0.036f, 0.19f );
v.SymbolA.LocalScale = new Vector3( 0.58f, 0.040f, 0.11f );
break;
case 2:
v.SymbolB.LocalScale = new Vector3( 0.68f, 0.036f, 0.17f );
v.SymbolB.LocalRotation = Rotation.FromYaw( 45f );
v.SymbolA.LocalScale = new Vector3( 0.56f, 0.040f, 0.10f );
v.SymbolA.LocalRotation = Rotation.FromYaw( 45f );
break;
case 3:
v.SymbolA.LocalScale = new Vector3( 0.60f, 0.040f, 0.12f );
v.SymbolB.LocalScale = new Vector3( 0.12f, 0.042f, 0.60f );
break;
case 4:
v.SymbolB.LocalScale = new Vector3( 0.42f, 0.036f, 0.42f );
v.SymbolA.LocalScale = new Vector3( 0.30f, 0.040f, 0.30f );
break;
default:
v.SymbolA.LocalScale = new Vector3( 0.60f, 0.040f, 0.12f );
v.SymbolA.LocalRotation = Rotation.FromYaw( -45f );
v.SymbolB.LocalScale = new Vector3( 0.60f, 0.042f, 0.12f );
v.SymbolB.LocalRotation = Rotation.FromYaw( 45f );
break;
}
}
void RefreshAllPieceVisuals()
{
if ( _visuals == null ) return;
for ( int y = 0; y < _visuals.GetLength( 1 ); y++ )
for ( int x = 0; x < _visuals.GetLength( 0 ); x++ )
ApplyPieceVisualStyle( _visuals[x, y] );
}
void RemoveCandyAt( int x, int y )
{
var v = _visuals[x, y];
v?.Go?.Destroy();
_visuals[x, y] = null;
}
Vector3 CellToWorld( int x, int y )
{
float ox = (x - (Width - 1) * 0.5f) * CellSpacing;
float oz = (y - (Height - 1) * 0.5f) * CellSpacing;
return BoardOrigin + new Vector3( ox, 0f, oz );
}
// -------------------- input --------------------
bool TryHitBoardCell( out int cx, out int cy )
{
cx = cy = -1;
if ( !TryGetMainCamera( out var cam ) ) return false;
var ray = cam.ScreenPixelToRay( Mouse.Position );
float planeY = BoardOrigin.y;
float denom = ray.Forward.y;
if ( MathF.Abs( denom ) < 0.0001f ) return false;
float t = (planeY - ray.Position.y) / denom;
if ( t <= 0 ) return false;
var hit = ray.Position + ray.Forward * t;
float localX = hit.x - BoardOrigin.x + (Width - 1) * 0.5f * CellSpacing;
float localZ = hit.z - BoardOrigin.z + (Height - 1) * 0.5f * CellSpacing;
int tx = (int)MathF.Floor( localX / CellSpacing + 0.5f );
int ty = (int)MathF.Floor( localZ / CellSpacing + 0.5f );
if ( tx < 0 || tx >= Width || ty < 0 || ty >= Height ) return false;
cx = tx; cy = ty;
return true;
}
void HandleMouse()
{
bool overBoard = TryHitBoardCell( out int cx, out int cy );
if ( overBoard )
{
if ( cx != _cursorX || cy != _cursorY )
ResetIdle();
_cursorX = cx;
_cursorY = cy;
}
bool mouseLeft = Input.Down( "attack1" );
bool justPressed = mouseLeft && !_prevMouseLeft;
bool justReleased = !mouseLeft && _prevMouseLeft;
_prevMouseLeft = mouseLeft;
if ( overBoard && (cx != _cursorX || cy != _cursorY) )
ResetIdle();
if ( justPressed && overBoard )
{
ResetIdle();
_dragging = true;
_dragSwapped = false;
_dragStartX = cx;
_dragStartY = cy;
_selectedX = cx;
_selectedY = cy;
_hasSelection = true;
SpawnShockwave( CellToWorld( cx, cy ), new Color( 1f, 1f, 1f ), 0.28f, 26f );
return;
}
if ( _dragging && mouseLeft && overBoard && !_dragSwapped )
{
if ( (cx != _dragStartX || cy != _dragStartY) && AreAdjacent( _dragStartX, _dragStartY, cx, cy ) )
{
BeginSwap( _dragStartX, _dragStartY, cx, cy );
_dragSwapped = true;
_hasSelection = false;
}
}
if ( justReleased )
{
_dragging = false;
}
}
void HandleInput()
{
if ( Pressed( "r" ) || ActionPressed( "reload" ) )
{
BuildFreshBoard();
Log.Info( "Board reset." );
return;
}
if ( PressedEither( "ArrowUp", "w" ) ) _cursorY = Math.Min( Height - 1, _cursorY + 1 );
if ( PressedEither( "ArrowDown", "s" ) ) _cursorY = Math.Max( 0, _cursorY - 1 );
if ( PressedEither( "ArrowLeft", "a" ) ) _cursorX = Math.Max( 0, _cursorX - 1 );
if ( PressedEither( "ArrowRight", "d" ) ) _cursorX = Math.Min( Width - 1, _cursorX + 1 );
HandleControllerCursor();
if ( Pressed( "Space" ) || Pressed( "Enter" ) || Pressed( "e" ) || ActionPressed( "jump", "use", "attack1" ) )
HandleSelectKey();
if ( _hasSelection && ActionPressed( "attack2", "duck" ) )
_hasSelection = false;
}
void HandleControllerCursor()
{
if ( !Input.UsingController && Input.ControllerCount <= 0 )
return;
if ( _controllerMoveCooldown > 0f )
return;
var move = Input.AnalogMove;
float ax = move.x;
float ay = move.y;
const float threshold = 0.45f;
bool moved = false;
if ( MathF.Abs( ax ) > MathF.Abs( ay ) && MathF.Abs( ax ) > threshold )
{
_cursorX = Math.Clamp( _cursorX + (ax > 0f ? 1 : -1), 0, Width - 1 );
moved = true;
}
else if ( MathF.Abs( ay ) > threshold )
{
_cursorY = Math.Clamp( _cursorY + (ay > 0f ? 1 : -1), 0, Height - 1 );
moved = true;
}
if ( moved )
_controllerMoveCooldown = 0.16f;
}
void HandleSelectKey()
{
if ( !_hasSelection )
{
_selectedX = _cursorX;
_selectedY = _cursorY;
_hasSelection = true;
SpawnShockwave( CellToWorld( _cursorX, _cursorY ), new Color( 1f, 1f, 1f ), 0.28f, 26f );
return;
}
if ( _selectedX == _cursorX && _selectedY == _cursorY )
{
_hasSelection = false;
return;
}
if ( !AreAdjacent( _selectedX, _selectedY, _cursorX, _cursorY ) )
{
SpawnShockwave( CellToWorld( _cursorX, _cursorY ), new Color( 0.22f, 0.92f, 1f ), 0.22f, 18f );
_selectedX = _cursorX;
_selectedY = _cursorY;
return;
}
BeginSwap( _selectedX, _selectedY, _cursorX, _cursorY );
_hasSelection = false;
}
// -------------------- hint system --------------------
void ResetIdle()
{
_idleTime = 0f;
_hasHint = false;
}
void UpdateHint()
{
// Hints only make sense while the player can actually act on the board.
if ( CurrentState != GameState.Playing || _phase != Phase.Idle || _hasSelection )
{
_idleTime = 0f;
_hasHint = false;
return;
}
_idleTime += Time.Delta;
if ( _idleTime < HintDelay )
{
_hasHint = false;
return;
}
if ( !_hasHint )
_hasHint = FindHintMove( out _hintAX, out _hintAY, out _hintBX, out _hintBY );
}
bool FindHintMove( out int ax, out int ay, out int bx, out int by )
{
ax = ay = bx = by = -1;
if ( _types == null ) return false;
// Prefer activating an existing special if one is on the board.
for ( int y = 0; y < Height; y++ )
{
for ( int x = 0; x < Width; x++ )
{
if ( _specials != null && _specials[x, y] != SpecialKind.None )
{
if ( x + 1 < Width ) { ax = x; ay = y; bx = x + 1; by = y; return true; }
if ( y + 1 < Height ) { ax = x; ay = y; bx = x; by = y + 1; return true; }
}
}
}
for ( int y = 0; y < Height; y++ )
{
for ( int x = 0; x < Width; x++ )
{
if ( x + 1 < Width && SwapWouldMatch( x, y, x + 1, y ) ) { ax = x; ay = y; bx = x + 1; by = y; return true; }
if ( y + 1 < Height && SwapWouldMatch( x, y, x, y + 1 ) ) { ax = x; ay = y; bx = x; by = y + 1; return true; }
}
}
return false;
}
// -------------------- phase machine --------------------
void BeginSwap( int x1, int y1, int x2, int y2 )
{
_hasEverSwapped = true;
ResetIdle();
PlayUiSound( SwapSound, 0.55f, 0.95f + Game.Random.Float( 0f, 0.12f ) );
_swapAX = x1; _swapAY = y1; _swapBX = x2; _swapBY = y2;
StartSwapTween( x1, y1, x2, y2 );
_phase = Phase.SwapForward;
_phaseTimer = 0f;
}
void StartSwapTween( int x1, int y1, int x2, int y2 )
{
var a = _visuals[x1, y1];
var b = _visuals[x2, y2];
if ( a == null || b == null ) return;
var posA = CellToWorld( x1, y1 );
var posB = CellToWorld( x2, y2 );
a.SwapFrom = a.Go.WorldPosition; a.SwapTo = posB; a.SwapT = 0f; a.Swapping = true; a.Anchor = posB;
b.SwapFrom = b.Go.WorldPosition; b.SwapTo = posA; b.SwapT = 0f; b.Swapping = true; b.Anchor = posA;
// Update the logical board now so subsequent logic is correct
(_types[x1, y1], _types[x2, y2]) = (_types[x2, y2], _types[x1, y1]);
(_specials[x1, y1], _specials[x2, y2]) = (_specials[x2, y2], _specials[x1, y1]);
(_visuals[x1, y1], _visuals[x2, y2]) = (_visuals[x2, y2], _visuals[x1, y1]);
}
void UpdatePhase()
{
_phaseTimer += Time.Delta;
switch ( _phase )
{
case Phase.Idle: return;
case Phase.SwapForward:
if ( _phaseTimer >= SwapDuration )
{
var matches = FindMatches();
var specialActivation = GetSpecialActivationForSwap();
foreach ( var idx in specialActivation )
matches.Add( idx );
if ( matches.Count == 0 )
{
PlayUiSound( DeniedSound, 0.55f, 0.82f );
SpawnShockwave( (CellToWorld( _swapAX, _swapAY ) + CellToWorld( _swapBX, _swapBY )) * 0.5f, new Color( 1f, 0.18f, 0.06f ), 0.34f, 52f );
PunchCamera( 1.0f, 0.85f );
// Swap back
StartSwapTween( _swapAX, _swapAY, _swapBX, _swapBY );
_phase = Phase.SwapBack;
_phaseTimer = 0f;
}
else
{
_moves++;
_comboCount = 1;
BeginClear( matches );
}
}
break;
case Phase.SwapBack:
if ( _phaseTimer >= SwapDuration )
{
_phase = Phase.Idle;
_phaseTimer = 0f;
}
break;
case Phase.Clearing:
if ( AllClearAnimsDone() )
{
DestroyClearedCandies();
_phase = Phase.Collapsing;
_phaseTimer = 0f;
CollapseColumns();
}
break;
case Phase.Collapsing:
if ( AllAtAnchor() )
{
_phase = Phase.Refilling;
_phaseTimer = 0f;
Refill();
}
break;
case Phase.Refilling:
if ( AllAtAnchor() )
{
_phase = Phase.Pause;
_phaseTimer = 0f;
}
break;
case Phase.Pause:
if ( _phaseTimer >= CascadePause )
{
var matches = FindMatches();
if ( matches.Count > 0 )
{
_comboCount++;
BeginClear( matches );
}
else
{
_phase = Phase.Idle;
CheckLevelEnd();
if ( CurrentState == GameState.Playing && !HasPossibleMove() )
{
// Levels rescue the player with a reshuffle; endless ends
// precisely when there is nothing left to match.
if ( IsEndless )
EndEndlessRun();
else
ReshuffleBoardForAvailableMove();
}
Log.Info( $"Score: {_score} | Moves: {_moves}" );
}
}
break;
}
}
void BeginClear( HashSet<int> matches )
{
bool activatingSpecial = ContainsSpecial( matches );
int triggeredSpecials = CountSpecialsInCells( matches );
var rewardKind = SpecialKind.None;
int rewardIdx = -1;
if ( !activatingSpecial )
TryChooseSpecialReward( matches, out rewardIdx, out rewardKind );
ExpandSpecialClears( matches );
if ( rewardIdx >= 0 && matches.Count > 3 )
matches.Remove( rewardIdx );
TrackObjectiveProgress( matches, rewardKind, triggeredSpecials );
_score += matches.Count * 10 * _comboCount + (activatingSpecial ? 75 * _comboCount : 0);
PlayUiSound( _comboCount > 1 ? ComboSound : ClearSound, MathF.Min( 1f, 0.55f + _comboCount * 0.12f ), 0.95f + _comboCount * 0.06f );
var center = Vector3.Zero;
int centerCount = 0;
if ( rewardIdx >= 0 && rewardKind != SpecialKind.None )
{
int sx = rewardIdx % Width;
int sy = rewardIdx / Width;
SetSpecialAt( sx, sy, rewardKind );
var specialVisual = _visuals[sx, sy];
if ( specialVisual != null )
{
SpawnShockwave( specialVisual.Go.WorldPosition, new Color( 1f, 0.88f, 0.18f ), 0.52f, 72f );
SpawnFireColumn( specialVisual.Go.WorldPosition, 0.65f );
}
}
foreach ( var idx in matches )
{
int x = idx % Width;
int y = idx / Width;
var v = _visuals[x, y];
if ( v == null ) continue;
v.Clearing = true;
v.ClearTimer = ClearDuration;
_specials[x, y] = SpecialKind.None;
var clearColor = PieceColor( v.Type );
SpawnBurstAt( v.Go.WorldPosition, clearColor );
SpawnShockwave( v.Go.WorldPosition, clearColor, 0.34f, 38f + _comboCount * 10f );
center += v.Go.WorldPosition;
centerCount++;
}
if ( centerCount > 0 )
{
SpawnFireColumn( center / centerCount, MathF.Min( 1.8f, 1f + matches.Count * 0.05f + _comboCount * 0.18f ) );
SpawnShockwave( center / centerCount, Color.White, 0.48f, 70f + matches.Count * 4f );
}
// Camera punch scales with combo size and chain depth
float punchFov = MathF.Min( 8f, 2.5f + matches.Count * 0.3f + _comboCount * 1.2f );
float punchShake = MathF.Min( 6f, 1.5f + _comboCount * 0.8f );
PunchCamera( punchFov, punchShake );
_phase = Phase.Clearing;
_phaseTimer = 0f;
}
HashSet<int> GetSpecialActivationForSwap()
{
var result = new HashSet<int>();
AddSpecialCells( _swapAX, _swapAY, result );
AddSpecialCells( _swapBX, _swapBY, result );
return result;
}
bool ContainsSpecial( HashSet<int> cells )
{
foreach ( var idx in cells )
{
int x = idx % Width;
int y = idx / Width;
if ( InBounds( x, y ) && _specials[x, y] != SpecialKind.None )
return true;
}
return false;
}
int CountSpecialsInCells( HashSet<int> cells )
{
int count = 0;
foreach ( var idx in cells )
{
int x = idx % Width;
int y = idx / Width;
if ( InBounds( x, y ) && _specials[x, y] != SpecialKind.None )
count++;
}
return count;
}
void TrackObjectiveProgress( HashSet<int> clearedCells, SpecialKind rewardKind, int triggeredSpecials )
{
if ( !HasSecondaryObjective )
return;
var level = CurrentLevelDefinition;
int add = 0;
switch ( level.ObjectiveKind )
{
case ColourBreakObjectiveKind.ClearPieces:
add = clearedCells.Count;
break;
case ColourBreakObjectiveKind.ClearColour:
foreach ( var idx in clearedCells )
{
int x = idx % Width;
int y = idx / Width;
if ( InBounds( x, y ) && _types[x, y] == level.ObjectiveType )
add++;
}
break;
case ColourBreakObjectiveKind.MakeSpecials:
add = rewardKind == SpecialKind.None ? 0 : 1;
break;
case ColourBreakObjectiveKind.TriggerSpecials:
add = triggeredSpecials;
break;
case ColourBreakObjectiveKind.ChainCombos:
add = _comboCount > 1 ? 1 : 0;
break;
}
if ( add > 0 )
_objectiveProgress = Math.Min( level.ObjectiveTarget, _objectiveProgress + add );
}
void ExpandSpecialClears( HashSet<int> cells )
{
bool changed;
int guard = 0;
do
{
changed = false;
var snapshot = new List<int>( cells );
foreach ( var idx in snapshot )
{
int before = cells.Count;
AddSpecialCells( idx % Width, idx / Width, cells );
if ( cells.Count > before ) changed = true;
}
}
while ( changed && ++guard < Width * Height );
}
void AddSpecialCells( int x, int y, HashSet<int> cells )
{
if ( !InBounds( x, y ) ) return;
switch ( _specials[x, y] )
{
case SpecialKind.RowBlast:
for ( int cx = 0; cx < Width; cx++ ) cells.Add( y * Width + cx );
break;
case SpecialKind.ColumnBlast:
for ( int cy = 0; cy < Height; cy++ ) cells.Add( cy * Width + x );
break;
case SpecialKind.Bomb:
for ( int cy = y - 2; cy <= y + 2; cy++ )
for ( int cx = x - 2; cx <= x + 2; cx++ )
if ( InBounds( cx, cy ) ) cells.Add( cy * Width + cx );
break;
}
}
void TryChooseSpecialReward( HashSet<int> matches, out int rewardIdx, out SpecialKind kind )
{
rewardIdx = -1;
kind = SpecialKind.None;
if ( matches.Count < 4 ) return;
int preferred = matches.Contains( _swapBX + _swapBY * Width ) ? _swapBX + _swapBY * Width :
matches.Contains( _swapAX + _swapAY * Width ) ? _swapAX + _swapAY * Width :
matches.First();
rewardIdx = preferred;
kind = matches.Count >= 5 ? SpecialKind.Bomb : SpecialKind.RowBlast;
int x = preferred % Width;
int y = preferred / Width;
int sameRow = 0;
int sameColumn = 0;
foreach ( var idx in matches )
{
if ( idx / Width == y ) sameRow++;
if ( idx % Width == x ) sameColumn++;
}
if ( matches.Count < 5 )
kind = sameColumn > sameRow ? SpecialKind.ColumnBlast : SpecialKind.RowBlast;
}
void SetSpecialAt( int x, int y, SpecialKind kind )
{
if ( !InBounds( x, y ) ) return;
_specials[x, y] = kind;
var v = _visuals[x, y];
if ( v == null ) return;
v.Special = kind;
UpdateSpecialVisual( v );
}
void UpdateSpecialVisual( CandyVisual v )
{
if ( v?.SpecialBadgeRenderer == null ) return;
v.SpecialBadgeRenderer.Tint = v.Special switch
{
SpecialKind.RowBlast => new Color( 1f, 0.86f, 0.12f, 1f ),
SpecialKind.ColumnBlast => new Color( 0.22f, 0.92f, 1f, 1f ),
SpecialKind.Bomb => new Color( 1f, 0.20f, 0.06f, 1f ),
_ => new Color( 1f, 1f, 1f, 0f )
};
v.SpecialBadge.LocalRotation = v.Special switch
{
SpecialKind.ColumnBlast => Rotation.FromYaw( 90f ),
SpecialKind.Bomb => Rotation.FromYaw( 45f ),
_ => Rotation.Identity
};
v.SpecialBadge.LocalScale = v.Special == SpecialKind.Bomb
? new Vector3( 0.22f, 0.04f, 0.22f )
: v.Special == SpecialKind.None
? Vector3.Zero
: new Vector3( 0.44f, 0.035f, 0.06f );
}
void SpawnBurstAt( Vector3 pos, Color color )
{
if ( !EffectsEnabled ) return;
int count = 24;
for ( int i = 0; i < count; i++ )
{
var go = RentBurstObject( "Colour Spark" );
go.WorldPosition = pos + new Vector3( Game.Random.Float( -4f, 4f ), Game.Random.Float( -3f, 3f ), Game.Random.Float( -4f, 4f ) );
go.WorldRotation = Rotation.Random;
bool hot = i % 3 != 0;
float scale = hot ? Game.Random.Float( 0.09f, 0.18f ) : Game.Random.Float( 0.045f, 0.10f );
go.WorldScale = hot ? new Vector3( scale * 0.75f, scale * 0.22f, scale * 1.9f ) : Vector3.One * scale;
var mr = go.GetComponent<ModelRenderer>();
mr.Model = hot ? (_boxModel ?? _sphereModel) : ((i % 2 == 0 && _sphereModel != null) ? _sphereModel : _boxModel);
var fireTint = i % 4 == 0 ? new Color( 1f, 0.18f, 0.02f ) : new Color( 1f, 0.70f, 0.10f );
mr.Tint = hot ? Color.Lerp( fireTint, color, 0.28f ) : Color.Lerp( color, Color.White, 0.45f );
float ang = (i / (float)count) * MathF.PI * 2f + Game.Random.Float( -0.4f, 0.4f );
float speed = hot ? Game.Random.Float( 180f, 420f ) : Game.Random.Float( 320f, 620f );
var vel = new Vector3( MathF.Cos( ang ) * speed, Game.Random.Float( -180f, -30f ), Game.Random.Float( 260f, 650f ) + MathF.Sin( ang ) * speed * 0.25f );
_bursts.Add( new Burst
{
Go = go,
Renderer = mr,
Velocity = vel,
Life = hot ? 0.55f : 0.82f,
MaxLife = hot ? 0.55f : 0.82f,
BaseTint = mr.Tint,
StartScale = scale,
} );
}
}
void SpawnFireColumn( Vector3 pos, float intensity )
{
if ( !EffectsEnabled ) return;
int count = (int)(18 * intensity);
for ( int i = 0; i < count; i++ )
{
var go = RentBurstObject( "Match Fire" );
go.WorldPosition = pos + new Vector3( Game.Random.Float( -18f, 18f ), Game.Random.Float( -8f, 4f ), Game.Random.Float( -8f, 8f ) );
go.WorldRotation = Rotation.Random;
float scale = Game.Random.Float( 0.12f, 0.28f ) * intensity;
go.WorldScale = new Vector3( scale * 0.75f, scale * 0.16f, scale * Game.Random.Float( 2.2f, 3.8f ) );
var mr = go.GetComponent<ModelRenderer>();
mr.Model = _boxModel ?? _sphereModel;
mr.Tint = i % 3 == 0 ? new Color( 1f, 0.95f, 0.35f, 0.95f ) : new Color( 1f, 0.28f, 0.03f, 0.85f );
_bursts.Add( new Burst
{
Go = go,
Renderer = mr,
Velocity = new Vector3( Game.Random.Float( -80f, 80f ), Game.Random.Float( -60f, 10f ), Game.Random.Float( 520f, 920f ) * intensity ),
Life = Game.Random.Float( 0.42f, 0.72f ),
MaxLife = 0.72f,
BaseTint = mr.Tint,
StartScale = scale,
} );
}
var lightGo = new GameObject( true, "Match Fire Light" );
lightGo.Flags = GameObjectFlags.NotNetworked;
lightGo.WorldPosition = pos + new Vector3( 0f, -18f, 22f );
var light = lightGo.AddComponent<PointLight>();
light.LightColor = new Color( 1f, 0.42f, 0.08f );
light.Radius = 360f * intensity;
_bursts.Add( new Burst
{
Go = lightGo,
Renderer = null,
Velocity = Vector3.Zero,
Life = 0.34f,
MaxLife = 0.34f,
BaseTint = Color.White,
StartScale = 1f,
} );
}
void AnimateBursts()
{
float dt = Time.Delta;
for ( int i = _bursts.Count - 1; i >= 0; i-- )
{
var b = _bursts[i];
b.Life -= dt;
if ( b.Life <= 0f || b.Go == null )
{
// Renderer bursts go back to the pool; the lone PointLight burst is destroyed.
if ( b.Go != null && b.Renderer != null )
ReturnBurstObject( b.Go );
else
b.Go?.Destroy();
_bursts.RemoveAt( i );
continue;
}
// gravity + drag
b.Velocity.z -= 1400f * dt;
b.Velocity *= 1f - dt * 1.4f;
b.Go.WorldPosition += b.Velocity * dt;
b.Go.WorldRotation *= Rotation.From( new Angles( dt * 540f, dt * 720f, dt * 360f ) );
float k = b.Life / b.MaxLife;
if ( b.Renderer != null )
{
b.Go.WorldScale *= 1f + dt * 0.55f;
var t = b.BaseTint;
t.a = k;
b.Renderer.Tint = t;
}
}
}
void SpawnShockwave( Vector3 pos, Color color, float life, float radius )
{
if ( !EffectsEnabled ) return;
var go = new GameObject( true, "ColourBreak Shockwave" );
go.Flags = GameObjectFlags.NotNetworked;
go.WorldPosition = pos + new Vector3( 0f, -3f, 0f );
go.WorldScale = new Vector3( 0.08f, 0.01f, 0.08f );
var mr = go.AddComponent<ModelRenderer>();
mr.Model = _sphereModel ?? _boxModel;
mr.Tint = new Color( color.r, color.g, color.b, 0.55f );
_shockwaves.Add( new Shockwave
{
Go = go,
Renderer = mr,
Tint = mr.Tint,
Life = life,
MaxLife = life,
Radius = radius,
} );
}
void AnimateShockwaves()
{
float dt = Time.Delta;
for ( int i = _shockwaves.Count - 1; i >= 0; i-- )
{
var wave = _shockwaves[i];
wave.Life -= dt;
if ( wave.Life <= 0f || wave.Go == null )
{
wave.Go?.Destroy();
_shockwaves.RemoveAt( i );
continue;
}
float k = 1f - wave.Life / wave.MaxLife;
float eased = EaseOutCubic( k );
float scale = MathX.Lerp( 0.04f, wave.Radius / 50f, eased );
wave.Go.WorldScale = new Vector3( scale, 0.012f, scale );
var tint = wave.Tint;
tint.a = 0.55f * (1f - k);
wave.Renderer.Tint = tint;
}
}
bool AllClearAnimsDone()
{
for ( int y = 0; y < Height; y++ )
for ( int x = 0; x < Width; x++ )
{
var v = _visuals[x, y];
if ( v != null && v.Clearing && v.ClearTimer > 0f ) return false;
}
return true;
}
void DestroyClearedCandies()
{
for ( int y = 0; y < Height; y++ )
for ( int x = 0; x < Width; x++ )
{
var v = _visuals[x, y];
if ( v != null && v.Clearing )
{
v.Go?.Destroy();
_visuals[x, y] = null;
_types[x, y] = Empty;
}
}
}
bool AllAtAnchor()
{
for ( int y = 0; y < Height; y++ )
for ( int x = 0; x < Width; x++ )
{
var v = _visuals[x, y];
if ( v == null ) continue;
if ( v.Spawning ) return false;
if ( (v.Go.WorldPosition - v.Anchor).LengthSquared > 4f ) return false;
}
return true;
}
void CollapseColumns()
{
for ( int x = 0; x < Width; x++ )
{
int writeY = 0;
for ( int y = 0; y < Height; y++ )
{
if ( _types[x, y] == Empty ) continue;
if ( y != writeY )
{
_types[x, writeY] = _types[x, y];
_types[x, y] = Empty;
_specials[x, writeY] = _specials[x, y];
_specials[x, y] = SpecialKind.None;
_visuals[x, writeY] = _visuals[x, y];
_visuals[x, y] = null;
var v = _visuals[x, writeY];
if ( v != null )
{
v.Anchor = CellToWorld( x, writeY );
v.Special = _specials[x, writeY];
UpdateSpecialVisual( v );
}
}
writeY++;
}
}
}
void Refill()
{
for ( int x = 0; x < Width; x++ )
{
int spawnIndex = 0;
for ( int y = 0; y < Height; y++ )
{
if ( _types[x, y] != Empty ) continue;
int type = Game.Random.Int( 0, CandyTypeCount - 1 );
_specials[x, y] = SpecialKind.None;
SpawnCandy( x, y, type, instant: false );
// Stagger spawn position so they form a falling stream
var v = _visuals[x, y];
if ( v != null )
{
var p = v.Go.WorldPosition;
p.z += spawnIndex * CellSpacing * 0.6f;
v.Go.WorldPosition = p;
}
spawnIndex++;
}
}
}
HashSet<int> FindMatches()
{
var result = new HashSet<int>();
for ( int y = 0; y < Height; y++ )
{
int runStart = 0;
for ( int x = 1; x <= Width; x++ )
{
bool atEnd = x == Width;
if ( !atEnd && _types[x, y] == _types[runStart, y] && _types[x, y] != Empty ) continue;
int runLen = x - runStart;
if ( _types[runStart, y] != Empty && runLen >= 3 )
for ( int rx = runStart; rx < x; rx++ ) result.Add( y * Width + rx );
runStart = x;
}
}
for ( int x = 0; x < Width; x++ )
{
int runStart = 0;
for ( int y = 1; y <= Height; y++ )
{
bool atEnd = y == Height;
if ( !atEnd && _types[x, y] == _types[x, runStart] && _types[x, y] != Empty ) continue;
int runLen = y - runStart;
if ( _types[x, runStart] != Empty && runLen >= 3 )
for ( int ry = runStart; ry < y; ry++ ) result.Add( ry * Width + x );
runStart = y;
}
}
return result;
}
bool HasPossibleMove()
{
if ( _types == null ) return false;
for ( int y = 0; y < Height; y++ )
{
for ( int x = 0; x < Width; x++ )
{
if ( _specials != null && _specials[x, y] != SpecialKind.None )
return true;
if ( x + 1 < Width && SwapWouldMatch( x, y, x + 1, y ) ) return true;
if ( y + 1 < Height && SwapWouldMatch( x, y, x, y + 1 ) ) return true;
}
}
return false;
}
bool SwapWouldMatch( int x1, int y1, int x2, int y2 )
{
(_types[x1, y1], _types[x2, y2]) = (_types[x2, y2], _types[x1, y1]);
bool result = IsMatchAt( x1, y1 ) || IsMatchAt( x2, y2 );
(_types[x1, y1], _types[x2, y2]) = (_types[x2, y2], _types[x1, y1]);
return result;
}
bool IsMatchAt( int x, int y )
{
int type = _types[x, y];
if ( type == Empty ) return false;
int count = 1;
for ( int cx = x - 1; cx >= 0 && _types[cx, y] == type; cx-- ) count++;
for ( int cx = x + 1; cx < Width && _types[cx, y] == type; cx++ ) count++;
if ( count >= 3 ) return true;
count = 1;
for ( int cy = y - 1; cy >= 0 && _types[x, cy] == type; cy-- ) count++;
for ( int cy = y + 1; cy < Height && _types[x, cy] == type; cy++ ) count++;
return count >= 3;
}
void ReshuffleBoardForAvailableMove()
{
var types = new List<int>( Width * Height );
for ( int y = 0; y < Height; y++ )
for ( int x = 0; x < Width; x++ )
if ( _types[x, y] != Empty ) types.Add( _types[x, y] );
for ( int attempt = 0; attempt < 40; attempt++ )
{
ShuffleList( types );
int i = 0;
for ( int y = 0; y < Height; y++ )
{
for ( int x = 0; x < Width; x++ )
{
_types[x, y] = types[i++ % types.Count];
_specials[x, y] = SpecialKind.None;
}
}
if ( FindMatches().Count == 0 && HasPossibleMove() )
break;
}
for ( int y = 0; y < Height; y++ )
{
for ( int x = 0; x < Width; x++ )
{
var v = _visuals[x, y];
if ( v == null ) continue;
v.Type = _types[x, y];
v.Special = SpecialKind.None;
v.Renderer.Model = ModelForType( v.Type );
ApplyPieceVisualStyle( v );
UpdateSpecialVisual( v );
}
}
SpawnShockwave( BoardOrigin, new Color( 0.25f, 0.92f, 1f ), 0.7f, 150f );
PunchCamera( 2f, 1.2f );
Log.Info( "Board reshuffled: no available moves." );
}
void EndEndlessRun()
{
CurrentState = GameState.Lost;
EndlessRecordSet = ColourBreakProgressStore.RecordEndlessScore( _saveData, _score );
try
{
Sandbox.Services.Stats.Increment( "endless-runs", 1 );
Sandbox.Services.Stats.SetValue( "endless-best", EndlessBest, "", null );
}
catch ( Exception e )
{
Log.Info( $"Leaderboard stats unavailable: {e.Message}" );
}
PlayUiSound( DeniedSound, 0.85f, 0.72f );
SpawnShockwave( BoardOrigin, new Color( 1f, 0.18f, 0.06f ), 0.7f, 150f );
PunchCamera( 3f, 2.4f );
}
void CheckLevelEnd()
{
if ( CurrentState != GameState.Playing ) return;
if ( IsEndless ) return; // endless ends only on a dead board
if ( _score >= TargetScore && ObjectiveComplete )
{
CurrentState = GameState.Won;
SaveWinProgress();
PlayUiSound( ComboSound, 1f, 1.18f );
SpawnFireColumn( BoardOrigin, 1.4f );
SpawnShockwave( BoardOrigin, new Color( 1f, 0.86f, 0.12f ), 0.85f, 190f );
PunchCamera( 5f, 3.2f );
return;
}
if ( _moves >= MoveLimit )
{
CurrentState = GameState.Lost;
PlayUiSound( DeniedSound, 0.85f, 0.72f );
SpawnShockwave( BoardOrigin, new Color( 1f, 0.18f, 0.06f ), 0.7f, 130f );
PunchCamera( 2.5f, 2.0f );
}
}
void ShuffleList( List<int> list )
{
for ( int i = list.Count - 1; i > 0; i-- )
{
int j = Game.Random.Int( 0, i );
(list[i], list[j]) = (list[j], list[i]);
}
}
int CountSpecials()
{
if ( _specials == null ) return 0;
int count = 0;
for ( int y = 0; y < Height; y++ )
for ( int x = 0; x < Width; x++ )
if ( _specials[x, y] != SpecialKind.None ) count++;
return count;
}
// -------------------- visuals --------------------
void AnimateCandies()
{
if ( _visuals == null ) return;
float dt = Time.Delta;
for ( int y = 0; y < Height; y++ )
{
for ( int x = 0; x < Width; x++ )
{
var v = _visuals[x, y];
if ( v == null || v.Go == null ) continue;
int t = v.Type;
var baseShape = PieceShape( t );
float baseScale = CandyScale;
// 1) Clearing animation: shrink + spin fast
if ( v.Clearing )
{
v.ClearTimer -= dt;
float k = MathF.Max( 0f, v.ClearTimer / ClearDuration );
float s = baseScale * EaseOutBack( k ) * 1.4f * k;
v.Go.WorldScale = baseShape * s;
v.Go.WorldRotation *= Rotation.FromYaw( dt * 720f );
v.Renderer.Tint = Color.Lerp( PieceColor( t ), Color.White, 1f - k );
continue;
}
// 2) Swap tween
if ( v.Swapping )
{
v.SwapT += dt / SwapDuration;
float k = MathF.Min( 1f, v.SwapT );
float e = EaseInOutCubic( k );
var pos = Vector3.Lerp( v.SwapFrom, v.SwapTo, e );
// Lift slightly off the board for an arc
pos.y -= MathF.Sin( k * MathF.PI ) * CellSpacing * 0.18f;
v.Go.WorldPosition = pos;
if ( k >= 1f ) { v.Swapping = false; v.Go.WorldPosition = v.SwapTo; v.Anchor = v.SwapTo; }
}
else
{
// 3) Falling toward anchor (used for collapse + refill)
var pos = v.Go.WorldPosition;
var to = v.Anchor;
var diff = to - pos;
if ( diff.LengthSquared > 1f )
{
// gravity-ish acceleration toward anchor on Z (vertical)
v.Velocity.z -= 4500f * dt;
pos.z += v.Velocity.z * dt;
pos.x = MathX.Lerp( pos.x, to.x, dt * 14f );
pos.y = MathX.Lerp( pos.y, to.y, dt * 14f );
if ( pos.z <= to.z )
{
pos.z = to.z;
// Squash on landing
v.Velocity.z = 0f;
}
v.Go.WorldPosition = pos;
}
else
{
v.Go.WorldPosition = to;
v.Velocity = Vector3.Zero;
}
}
// 4) Spawn pop-in
float spawnScale = 1f;
if ( v.Spawning )
{
v.SpawnTimer -= dt;
float k = MathF.Max( 0f, v.SpawnTimer / 0.25f );
spawnScale = MathX.Lerp( 1f, 0.4f, k );
if ( v.SpawnTimer <= 0f ) v.Spawning = false;
}
// 5) Idle bob + gentle spin
v.BobPhase += dt;
float bob = MathF.Sin( v.BobPhase * 2.4f ) * 1.2f;
if ( !v.Swapping && (v.Go.WorldPosition - v.Anchor).LengthSquared < 4f )
{
var p = v.Anchor;
p.y -= bob;
v.Go.WorldPosition = p;
}
v.Go.WorldRotation *= Rotation.FromYaw( dt * (10f + (t * 3f)) );
// 6) Final scale (with highlight applied later in ApplyHighlights)
v.Go.WorldScale = baseShape * baseScale * spawnScale;
}
}
}
void ApplyHighlights()
{
if ( _visuals == null ) return;
for ( int y = 0; y < Height; y++ )
{
for ( int x = 0; x < Width; x++ )
{
var v = _visuals[x, y];
if ( v == null || v.Renderer == null || v.Clearing ) continue;
var baseTint = PieceColor( v.Type );
bool isCursor = x == _cursorX && y == _cursorY;
bool isSelected = _hasSelection && x == _selectedX && y == _selectedY;
bool isLegalNeighbour = _hasSelection && !isSelected && AreAdjacent( _selectedX, _selectedY, x, y );
bool isHint = _hasHint && ((x == _hintAX && y == _hintAY) || (x == _hintBX && y == _hintBY));
float scaleMul = 1f;
if ( isHint && !isCursor && !isSelected )
{
float p = 0.5f + 0.5f * MathF.Sin( Time.Now * 7f );
baseTint = Color.Lerp( baseTint, Color.White, 0.30f + 0.45f * p );
scaleMul += 0.18f + 0.16f * p;
}
if ( isCursor )
{
float p = 0.5f + 0.5f * MathF.Sin( Time.Now * 12f );
// Instead of a box, pulse the candy itself significantly brighter and bigger
baseTint = Color.Lerp( baseTint, Color.White, 0.40f + 0.40f * p );
scaleMul += 0.25f + 0.15f * p;
}
if ( isSelected )
{
// Selection gets a distinct yellow overlay
baseTint = Color.Lerp( baseTint, new Color( 1f, 0.95f, 0.2f ), 0.60f );
scaleMul += 0.35f;
}
if ( isLegalNeighbour )
{
float p = 0.5f + 0.5f * MathF.Sin( Time.Now * 9f );
baseTint = Color.Lerp( baseTint, new Color( 0.22f, 0.92f, 1f ), 0.18f + 0.14f * p );
scaleMul += 0.08f + 0.05f * p;
}
v.Renderer.Tint = baseTint;
if ( v.RimRenderer != null )
v.RimRenderer.Tint = Color.Lerp( baseTint, Color.Black, isSelected ? 0.12f : isLegalNeighbour ? 0.22f : 0.36f );
if ( v.FaceRenderer != null )
v.FaceRenderer.Tint = Color.Lerp( baseTint, Color.White, isCursor || isSelected || isLegalNeighbour ? 0.45f : 0.20f );
if ( v.AccentRenderer != null )
v.AccentRenderer.Tint = Color.Lerp( baseTint, Color.White, isCursor || isSelected || isLegalNeighbour ? 0.82f : 0.52f );
if ( v.ShineRenderer != null )
v.ShineRenderer.Tint = isCursor || isSelected || isLegalNeighbour ? new Color( 1f, 1f, 1f, 1f ) : new Color( 1f, 1f, 1f, 0.74f );
if ( v.Halo != null )
{
float p = 0.5f + 0.5f * MathF.Sin( Time.Now * 10f );
if ( isSelected )
{
v.Halo.LocalScale = new Vector3( 1.34f + 0.05f * p, 0.055f, 1.34f + 0.05f * p );
if ( v.HaloRenderer != null ) v.HaloRenderer.Tint = new Color( 1f, 0.86f, 0.08f, 0.90f );
}
else if ( isCursor )
{
v.Halo.LocalScale = new Vector3( 1.20f + 0.05f * p, 0.040f, 1.20f + 0.05f * p );
if ( v.HaloRenderer != null ) v.HaloRenderer.Tint = new Color( 1f, 1f, 1f, 0.68f );
}
else
{
v.Halo.LocalScale = Vector3.Zero;
}
}
if ( v.Guide != null )
{
float p = 0.5f + 0.5f * MathF.Sin( Time.Now * 8f );
if ( isLegalNeighbour )
{
v.Guide.LocalScale = new Vector3( 1.16f + 0.07f * p, 0.030f, 1.16f + 0.07f * p );
if ( v.GuideRenderer != null ) v.GuideRenderer.Tint = new Color( 0.22f, 0.92f, 1f, 0.62f );
}
else
{
v.Guide.LocalScale = Vector3.Zero;
}
}
if ( !v.Swapping && !v.Spawning )
v.Go.WorldScale = PieceShape( v.Type ) * CandyScale * scaleMul;
}
}
}
int PickTypeAvoidingImmediateMatch( int x, int y )
{
for ( int attempt = 0; attempt < 12; attempt++ )
{
int type = Game.Random.Int( 0, CandyTypeCount - 1 );
bool badH = x >= 2 && _types[x - 1, y] == type && _types[x - 2, y] == type;
bool badV = y >= 2 && _types[x, y - 1] == type && _types[x, y - 2] == type;
if ( !badH && !badV ) return type;
}
return Game.Random.Int( 0, CandyTypeCount - 1 );
}
// -------------------- helpers --------------------
bool AreAdjacent( int x1, int y1, int x2, int y2 ) => Math.Abs( x1 - x2 ) + Math.Abs( y1 - y2 ) == 1;
bool InBounds( int x, int y ) => x >= 0 && x < Width && y >= 0 && y < Height;
bool PressedEither( string a, string b ) => Pressed( a ) || Pressed( b );
bool ActionPressed( params string[] actions )
{
foreach ( var action in actions )
if ( Input.Pressed( action ) )
return true;
return false;
}
bool Pressed( string key )
{
bool down = Input.Keyboard.Down( key );
_prevKeys.TryGetValue( key, out bool prev );
_prevKeys[key] = down;
return down && !prev;
}
void ClearBoardObjects()
{
if ( _visuals == null ) return;
for ( int y = 0; y < _visuals.GetLength( 1 ); y++ )
for ( int x = 0; x < _visuals.GetLength( 0 ); x++ )
_visuals[x, y]?.Go?.Destroy();
foreach ( var burst in _bursts )
{
if ( burst.Go != null && burst.Renderer != null )
ReturnBurstObject( burst.Go );
else
burst.Go?.Destroy();
}
foreach ( var wave in _shockwaves ) wave.Go?.Destroy();
_bursts.Clear();
_shockwaves.Clear();
}
void CleanupDecor()
{
foreach ( var go in _decor ) go?.Destroy();
_decor.Clear();
}
bool TryGetMainCamera( out CameraComponent cam )
{
cam = null;
if ( Scene == null ) return false;
foreach ( var c in Scene.GetAllComponents<CameraComponent>() )
{
if ( c.IsMainCamera ) { cam = c; return true; }
cam ??= c;
}
return cam != null;
}
void FrameMainCameraToBoard()
{
if ( !TryGetMainCamera( out var cam ) || cam.GameObject == null ) return;
float boardW = Width * CellSpacing;
float boardH = Height * CellSpacing;
float verticalFov = 44f;
float verticalFovRad = verticalFov * MathF.PI / 180f;
float aspect = 16f / 9f;
float targetW = boardW + CellSpacing * 4.8f;
float targetH = boardH + CellSpacing * 6.4f;
float distanceForHeight = (targetH * 0.5f) / MathF.Tan( verticalFovRad * 0.5f );
float horizontalFovRad = 2f * MathF.Atan( MathF.Tan( verticalFovRad * 0.5f ) * aspect );
float distanceForWidth = (targetW * 0.5f) / MathF.Tan( horizontalFovRad * 0.5f );
float distance = MathF.Max( distanceForHeight, distanceForWidth ) * 1.45f;
var center = BoardOrigin + new Vector3( 0f, 2f, 0f );
var camPos = center + new Vector3( 0f, -distance, 0f );
cam.GameObject.WorldPosition = camPos;
cam.GameObject.WorldRotation = Rotation.LookAt( center - camPos, Vector3.Up );
cam.FieldOfView = verticalFov;
cam.BackgroundColor = new Color( 0.055f, 0.045f, 0.095f );
cam.EnablePostProcessing = true;
cam.ZNear = 1f;
cam.ZFar = MathF.Max( cam.ZFar, 20000f );
_activeCamera = cam;
_baseFov = cam.FieldOfView;
_baseCameraPos = cam.GameObject.WorldPosition;
}
void UpdateAmbience()
{
float t = Time.Now;
if ( _orbitLightL != null )
{
float r = MathF.Max( Width, Height ) * CellSpacing * 0.7f;
_orbitLightL.WorldPosition = BoardOrigin + new Vector3( MathF.Cos( t * 0.7f ) * r, -CellSpacing * 1.2f, MathF.Sin( t * 0.7f ) * r );
}
if ( _orbitLightR != null )
{
float r = MathF.Max( Width, Height ) * CellSpacing * 0.7f;
_orbitLightR.WorldPosition = BoardOrigin + new Vector3( MathF.Cos( t * 0.7f + MathF.PI ) * r, -CellSpacing * 1.2f, MathF.Sin( t * 0.7f + MathF.PI ) * r );
}
if ( _trayInnerRenderer != null )
{
float r = 0.05f + 0.025f * MathF.Sin( t * 0.55f );
float g = 0.07f + 0.025f * MathF.Sin( t * 0.55f + 2f );
float b = 0.12f + 0.035f * MathF.Sin( t * 0.55f + 4f );
var shift = new Color( r, g, b );
_trayInnerRenderer.Tint = Color.Lerp( _trayBaseColor, shift, 0.45f );
}
}
void UpdateCameraPunch()
{
if ( _activeCamera == null ) return;
float dt = Time.Delta;
_fovPunch = MathX.Lerp( _fovPunch, 0f, dt * 6f );
_shakeAmount = MathX.Lerp( _shakeAmount, 0f, dt * 5f );
_activeCamera.FieldOfView = _baseFov + _fovPunch;
if ( _shakeAmount > 0.01f && _activeCamera.GameObject != null )
{
var offset = new Vector3(
Game.Random.Float( -1f, 1f ),
Game.Random.Float( -1f, 1f ),
Game.Random.Float( -1f, 1f ) ) * _shakeAmount;
_activeCamera.GameObject.WorldPosition = _baseCameraPos + offset;
}
else if ( _activeCamera.GameObject != null )
{
_activeCamera.GameObject.WorldPosition = _baseCameraPos;
}
}
void PunchCamera( float fovDelta, float shake )
{
if ( !CameraShakeEnabled ) return;
_fovPunch -= fovDelta; // negative FOV = zoom in
_shakeAmount = MathF.Max( _shakeAmount, shake );
}
// Seamless-ish looping background music. Re-triggers when the clip ends; the
// synthesized loop wraps at matching amplitude so the seam isn't audible.
void UpdateMusic()
{
if ( !MusicEnabled || string.IsNullOrWhiteSpace( MusicSound ) )
{
if ( _music.IsValid() )
{
_music.Stop( 0.4f );
_music = default;
}
return;
}
if ( !_music.IsValid() || _music.IsStopped || _music.Finished )
{
try
{
_music = Sound.Play( MusicSound );
if ( _music.IsValid() )
{
_music.SpacialBlend = 0f;
_music.Volume = MusicVolume;
}
}
catch
{
// Music asset missing/unsupported — silently continue without it.
}
}
else
{
_music.Volume = MusicVolume;
}
}
void PlayUiSound( string sound, float volume, float pitch )
{
if ( !AudioEnabled ) return;
if ( string.IsNullOrWhiteSpace( sound ) || _audioCooldown > 0f ) return;
try
{
var handle = Sound.Play( sound, 0f );
handle.Volume = volume;
handle.Pitch = pitch;
handle.SpacialBlend = 0f;
_audioCooldown = 0.035f;
}
catch
{
_audioCooldown = 0.2f;
}
}
void EnsureSceneLighting()
{
if ( Scene == null ) return;
foreach ( var l in Scene.GetAllComponents<DirectionalLight>() )
if ( l != null ) return;
var lightGo = new GameObject( true, "Auto Directional Light" );
lightGo.Flags = GameObjectFlags.NotNetworked;
lightGo.WorldRotation = Rotation.LookAt( new Vector3( 0.4f, -0.25f, -1f ).Normal, Vector3.Up );
var d = lightGo.AddComponent<DirectionalLight>();
d.LightColor = new Color( 0.95f, 0.98f, 1f );
d.Shadows = true;
}
// -------------------- easing --------------------
static float EaseInOutCubic( float t ) => t < 0.5f ? 4f * t * t * t : 1f - MathF.Pow( -2f * t + 2f, 3f ) / 2f;
static float EaseOutCubic( float t ) => 1f - MathF.Pow( 1f - t, 3f );
static float EaseOutBack( float t )
{
const float c1 = 1.70158f;
const float c3 = c1 + 1f;
float u = t - 1f;
return 1f + c3 * u * u * u + c1 * u * u;
}
}
Game
game
.switchcontrol
{
flex-direction: row;
width: 100px;
min-height: 24px;
align-items: center;
cursor: pointer;
.switch-frame
{
flex-grow: 0;
flex-shrink: 1;
width: 48px;
height: 16px;
background-color: #fff1;
margin: 0px 5px;
align-items: center;
border-radius: 100px;
transition: all 0.4s linear;
.switch-inner
{
position: relative;
flex-grow: 0;
flex-shrink: 1;
background-color: #999;
width: 25px;
height: 25px;
border-radius: 100px;
left: 20%;
transform: translateX( -50% );
transition: all 0.3s ease-out;
}
}
&.active
{
.switch-frame
{
background-color: #fffa;
}
.switch-inner
{
left: 80%;
background-color: #fff;
}
}
}
Game
game
.colorproperty
{
align-items: center;
position: relative;
.colorsquare
{
width: 20px;
height: 20px;
border-radius: 4px;
margin-right: 8px;
position: absolute;
left: 7px;
z-index: 1;
cursor: pointer;
}
> .textentry:not( .a.b.c )
{
padding-left: 36px;
}
}
Game
game
menupanel
{
position: absolute;
z-index: 1000;
pointer-events: all;
font-size: 12px;
flex-shrink: 0;
.background
{
position: absolute;
left: -5000px;
right: -5000px;
top: -5000px;
bottom: -5000px;
}
> .inner
{
min-width: 200px;
min-height: 20px;
flex-direction: column;
font-family: Poppins;
font-weight: bold;
border-radius: 10px;
box-shadow: 5px 5px 30px #000e;
background-color: #2a2a2a;
flex-shrink: 0;
.spacer
{
height: 1px;
background-color: #0005;
}
.option
{
color: #fffa;
padding: 0px 8px;
cursor: pointer;
flex-shrink: 0;
height: 32px;
&:first-child
{
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
&:last-child
{
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
.icon
{
padding: 8px;
font-family: Material Icons;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.text
{
padding: 8px;
flex-shrink: 0;
}
&:hover
{
background-color: #3472e6;
color: #f5f8fe;
}
}
}
}
Game
game
ColorControl
{
gap: 0.5rem;
flex-grow: 1;
pointer-events: all;
background-color: #000a;
border-radius: 4px;
padding: 2px;
height: 32px;
}
ColorControl TextEntry
{
flex-grow: 1;
flex-shrink: 0;
color: #aaa;
font-size: 1.2rem;
&:hover, &:focus
{
color: #ddd;
.icon
{
color: #3af;
}
}
&:active
{
color: white;
}
}
ColorControl > .colorswatch
{
aspect-ratio: 1;
border-radius: 4px;
height: 100%;
flex-shrink: 0;
cursor: pointer;
border: 2px solid #000;
}
Game
game
@import "/styles/_theme.scss";
.packagefilter
{
width: 100%;
flex-shrink: 0;
.left
{
flex-grow: 1;
flex-shrink: 0;
gap: 16px;
}
.right
{
flex-grow: 1;
flex-shrink: 0;
gap: 16px;
}
}
.packagefilter textentry
{
flex-grow: 1;
padding: 8px 12px;
pointer-events: all;
Label
{
margin-left: 26px;
}
iconpanel
{
position: absolute;
left: 12px;
opacity: 0.3;
font-size: $text-large;
}
}
.packagefilterfacet, .packagefilter textentry
{
background-color: rgba($default-950, 0.3);
color: $default-text;
outline: 1px solid $default-border;
transition: all 150ms $easing;
border-radius: $rounding-default;
padding: 4px 12px;
gap: 16px;
flex-shrink: 0;
font-size: 14px;
cursor: pointer;
align-items: center;
.clear-button
{
height: 100%;
margin-right: -8px;
align-items: center;
padding: 0px 4px;
&:hover
{
color: red;
border-radius: 100%;
}
}
&.is-active
{
background-color: $default-700;
}
&:not(.is-active)
{
&:hover
{
outline: 1px solid $default-50;
}
}
}
Debug: View Raw JSON Response
{
"TotalCount": 41,
"Files": [
{
"Ident": "verarp.colour_break",
"Path": "styles/form/_switch.scss",
"FileName": "_switch.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "\r\n$primary: red !default;\r\n$primary-alt: white !default;\r\n\r\n$switch-padding: 6px !default;\r\n\r\n.checkbox.switch\r\n{\r\n\tcursor: pointer;\r\n\r\n\t> .checkmark\r\n\t{\r\n\t\tfont-size: 22px;\r\n\t\tborder: 0px solid $primary;\r\n\t\tborder-radius: 100px;\r\n\t\ttext-align: center;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tcolor: $primary-alt;\r\n\t\tpadding: $switch-padding;\r\n\t\tpadding-right: 32px;\r\n\t\tpadding-left: $switch-padding;\r\n\t\ttransition: all 0.3s ease;\r\n\t\tbackground-color: rgba( $primary, 0.1 );\r\n\r\n\t\t> .handle\r\n\t\t{\r\n\t\t\tbackground-color: $primary-alt;\r\n\t\t\twidth: 20px;\r\n\t\t\theight: 20px;\r\n\t\t\tborder-radius: 100px;\r\n\t\t\tbox-shadow: 2px 2px 12px black;\r\n\t\t}\r\n\t}\r\n\r\n\t&.checked\r\n\t{\r\n\t\t> .checkmark\r\n\t\t{\r\n\t\t\tbackground-color: $primary;\r\n\t\t\tpadding-left: 32px;\r\n\t\t\tpadding-right: $switch-padding;\r\n\t\t}\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\ttransform: scale( 0.9 );\r\n\t\ttransform-origin: 20px 50%;\r\n\t}\r\n}\r\n"
},
{
"Ident": "verarp.colour_break",
"Path": "ui/controls/slidercontrol.razor.scss",
"FileName": "slidercontrol.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "\r\n.slidercontrol\r\n{\r\n flex-direction: row;\r\n min-width: 50px;\r\n position: relative;\r\n flex-shrink: 0;\r\n flex-direction: row;\r\n cursor: pointer;\r\n gap: 8px;\r\n flex-grow: 1;\r\n align-items: center;\r\n pointer-events: all;\r\n\r\n > .inner\r\n {\r\n flex-direction: column;\r\n flex-shrink: 1;\r\n flex-grow: 1;\r\n min-height: 32px;\r\n justify-content: center;\r\n\r\n > .values\r\n {\r\n width: 100%;\r\n pointer-events: none;\r\n font-size: 14px;\r\n color: #aaa;\r\n\r\n > .left\r\n {\r\n flex-grow: 1;\r\n }\r\n }\r\n\r\n > .track\r\n {\r\n position: relative;\r\n background-color: #888;\r\n height: 7px;\r\n margin: 8px;\r\n align-items: center;\r\n border-radius: 4px;\r\n\r\n > .track-active\r\n {\r\n background-color: #fff;\r\n position: absolute;\r\n height: 100%;\r\n left: 0px;\r\n border-radius: 4px;\r\n }\r\n\r\n > .thumb\r\n {\r\n position: relative;\r\n background-color: #fff;\r\n border-radius: 100px;\r\n width: 16px;\r\n height: 16px;\r\n transform: translateX( -50% );\r\n }\r\n }\r\n }\r\n\r\n > .entry\r\n {\r\n flex-shrink: 0;\r\n flex-grow: 0;\r\n width: 50px;\r\n\r\n > numberentry\r\n {\r\n background-color: transparent;\r\n\r\n > .content-label\r\n {\r\n padding: 0 4px;\r\n }\r\n }\r\n }\r\n}\r\n\r\n\r\n.slidercontrol .value-tooltip\r\n{\r\n position: absolute;\r\n bottom: 150%;\r\n left: -8px;\r\n z-index: 1000;\r\n flex-direction: column;\r\n\r\n > .label\r\n { \r\n background-color: black;\r\n padding: 8px 12px;\r\n border-radius: 8px;\r\n }\r\n\r\n >.tail\r\n {\r\n bottom: -0px;\r\n background-color: black;\r\n width: 10px;\r\n height: 10px;\r\n transform: rotateZ(45 deg) translateX( 4px );\r\n position: absolute;\r\n }\r\n}"
},
{
"Ident": "verarp.colour_break",
"Path": "ui/controls/color/coloralphacontrol.cs.scss",
"FileName": "coloralphacontrol.cs.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "ColorAlphaControl\r\n{\r\n\tgap: 0.5rem;\r\n\tflex-grow: 1;\r\n\tpointer-events: all;\r\n\tbackground: linear-gradient( to right, black, white );\r\n\tborder-radius: 4px;\r\n\tpadding: 2px;\r\n\theight: 12px;\r\n\tposition: relative;\r\n\tcursor: pointer;\r\n\tborder: 1px solid #333;\r\n\r\n\t&:hover\r\n\t{\r\n\t\tborder: 1px solid #08f;\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\tborder: 1px solid #fff;\r\n\t}\r\n\r\n\t.handle\r\n\t{\r\n\t\ttop: -5px;\r\n\t\tbottom: -5px;\r\n\t\taspect-ratio: 1;\r\n\t\tborder-radius: 100px;\r\n\t\tborder: 2px solid #444;\r\n\t\tposition: absolute;\r\n\t\tbackground-color: white;\r\n\t\tbox-shadow: 2px 2px 16px #000a;\r\n\t\ttransform: translateX( -50% );\r\n\t\tpointer-events: none;\r\n\t}\r\n}\r\n"
},
{
"Ident": "verarp.colour_break",
"Path": "styles/rootpanel.scss",
"FileName": "rootpanel.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "\r\n\r\nRootPanel\r\n{\r\n\tfont-family: Roboto;\r\n\tcolor: white;\r\n}\r\n\r\nIconPanel, i\r\n{\r\n\tfont-family: Material Icons;\r\n\tbackground-position: center;\r\n\tbackground-size: contain;\r\n}\r\n\r\n.tooltip\r\n{\r\n\tbackground-color: rgba( black, 0.8 );\r\n\tborder: 1px solid rgba( #555, 0.1 );\r\n\tpadding: 10px;\r\n\tborder-radius: 4px;\r\n\tcolor: white;\r\n\tfont-family: Inter;\r\n\tfont-weight: bold;\r\n\ttransition: opacity 0.09s ease;\r\n\topacity: 1;\r\n\tmargin: 0px;\r\n\r\n\t&:intro, &:outro\r\n\t{\r\n\t\topacity: 0;\r\n\t}\r\n}\r\n\r\n.textentry\r\n{\r\n\tcursor: text;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\twhite-space: nowrap;\r\n\toverflow: hidden;\r\n\tflex-direction: row;\r\n\tposition: relative;\r\n\tpadding: 10px;\r\n\r\n\t&.disabled\r\n\t{\r\n\t\tcursor: default;\r\n\t}\r\n\r\n\t&.is-multiline\r\n\t{\r\n\t\talign-items: flex-start;\r\n\t\twhite-space: normal;\r\n\t}\r\n\r\n\t.content-label,\r\n\t.placeholder\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t}\r\n\r\n\t.placeholder\r\n\t{\r\n\t\topacity: 0.2;\r\n\t\tpointer-events: none;\r\n\t}\r\n\r\n\t.iconpanel\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tright: 0;\r\n\t\tpadding-right: 10px;\r\n\t\tpointer-events: none;\r\n\r\n\t\t&.clearbutton\r\n\t\t{\r\n\t\t\tcolor: #fff;\r\n\t\t\tpointer-events: all;\r\n\t\t\tcursor: pointer;\r\n\t\t\topacity: 0.1;\r\n\r\n\t\t\t&:hover\r\n\t\t\t{\r\n\t\t\t\topacity: 0.5;\r\n\t\t\t}\r\n\r\n\t\t\t&:active\r\n\t\t\t{\r\n\t\t\t\topacity: 1;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t&:empty\r\n\t{\r\n\t\t.clearbutton\r\n\t\t{\r\n\t\t\tdisplay: none;\r\n\t\t}\r\n\t}\r\n\r\n\t&.has-icon\r\n\t{\r\n\t\tpadding-right: 30px;\r\n\t}\r\n\r\n\t.content-label,\r\n\t.prefix-label,\r\n\t.suffix-label,\r\n\t.placeholder\r\n\t{\r\n\t\toverflow: hidden;\r\n\t}\r\n\r\n\t.content-label\r\n\t{\r\n\t\torder: 1;\r\n\t}\r\n\r\n\t.suffix-label\r\n\t{\r\n\t\torder: 2;\r\n\t}\r\n\r\n\t.prefix-label,\r\n\t.suffix-label\r\n\t{\r\n\t\tflex-shrink: 0;\r\n\t\topacity: 0.5;\r\n\t}\r\n\r\n\t&.invalid\r\n\t{\r\n\t\tbox-shadow: 0px 0px 1px 2px #f8356b;\r\n\t}\r\n}\r\n\r\n.popup-panel\r\n{\r\n\tbackground-color: #1b202c;\r\n\tflex-direction: column;\r\n\tcolor: #8e9ec2;\r\n\ttransition: all 0.1s ease-out;\r\n\tposition: absolute;\r\n\tz-index: 2000;\r\n\tmin-height: 20px;\r\n\tmin-width: 10px;\r\n\toverflow: scroll;\r\n\tpointer-events: all;\r\n\tbox-shadow: 3px 3px 64px #000;\r\n\tborder-radius: 4px;\r\n\r\n\t&.medium\r\n\t{\r\n\t\tmax-width: 300px;\r\n\t}\r\n\r\n\t&.center-horizontal\r\n\t{\r\n\t\ttransform-origin: top center;\r\n\t}\r\n\r\n\t&.below-center\r\n\t{\r\n\t\ttransform: translateX( -50% );\r\n\t\ttransform-origin: 0% 0%;\r\n\t}\r\n\r\n\t&.above-left\r\n\t{\r\n\t\ttransform-origin: 0% 100%;\r\n\t}\r\n\r\n\t&.above-center\r\n\t{\r\n\t\ttransform: translateX( -50% );\r\n\t\ttransform-origin: 0% 100%;\r\n\t}\r\n\r\n\t&.left\r\n\t{\r\n\t\ttransform-origin: 100% 50%;\r\n\t\ttransform: translate( 00% -50% );\r\n\t}\r\n\r\n\t&.right\r\n\t{\r\n\t\ttransform-origin: 0 50%;\r\n\t\ttransform: translate( 0% -50% );\r\n\t}\r\n\r\n\t&:intro\r\n\t{\r\n\t\ttransform: scale( 0 );\r\n\t\tpointer-events: none;\r\n\t\topacity: 0;\r\n\t}\r\n\r\n\t&:outro\r\n\t{\r\n\t\topacity: 0;\r\n\t\ttransition: all 0.1s ease-in;\r\n\t\tmargin-top: 50px;\r\n\t\tpointer-events: none;\r\n\t}\r\n\r\n\t&.below-stretch\r\n\t{\r\n\t\ttransform-origin: 0% 0%;\r\n\r\n\t\t&:intro\r\n\t\t{\r\n\t\t\ttransform: scaleY( 0.1 );\r\n\t\t}\r\n\t}\r\n\r\n\t> .canvas\r\n\t{\r\n\t\tflex-direction: column;\r\n\t}\r\n\r\n\t.information\r\n\t{\r\n\t\tpadding: 16px;\r\n\t\tfont-size: 14px;\r\n\t\topacity: 0.5;\r\n\t}\r\n\r\n\tbutton\r\n\t{\r\n\t\tpadding: 2px 8px;\r\n\t\tcursor: pointer;\r\n\t\tfont-family: Poppins;\r\n\t\tfont-weight: 600;\r\n\t\tflex-shrink: 0;\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\tbackground-color: #08f;\r\n\t\t\tcolor: #fff;\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "verarp.colour_break",
"Path": "colourbreakmenu.razor.scss",
"FileName": "colourbreakmenu.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": ".cb-ui {\n position: absolute;\n inset: 0px;\n width: 100%;\n height: 100%;\n font-family: \"Inter\", \"Poppins\", \"Segoe UI\", sans-serif;\n color: #ffffff;\n pointer-events: none;\n}\n\n.cb-loading {\n position: absolute;\n top: 24px;\n left: 24px;\n padding: 10px 14px;\n font-size: 18px;\n font-weight: 800;\n color: #ffffff;\n background-color: rgba(5, 8, 20, 0.88);\n border: 1px solid rgba(255, 255, 255, 0.18);\n border-radius: 8px;\n}\n\n.cb-screen {\n position: absolute;\n inset: 0px;\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 28px;\n pointer-events: all;\n background-color: rgba(4, 6, 16, 0.70);\n}\n\n.cb-main {\n justify-content: flex-start;\n gap: 0px;\n padding: 24px 30px 28px;\n background-color: rgba(5, 7, 18, 0.18);\n}\n\n.cb-menu-top {\n width: 100%;\n display: flex;\n flex-direction: row;\n align-items: flex-start;\n justify-content: space-between;\n gap: 24px;\n}\n\n.cb-menu-metrics {\n display: flex;\n flex-direction: row;\n gap: 10px;\n}\n\n.cb-menu-metric {\n min-width: 98px;\n padding: 8px 12px;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n background-color: rgba(4, 8, 20, 0.84);\n border: 1px solid rgba(255, 255, 255, 0.20);\n border-radius: 8px;\n box-shadow: 0px 10px 24px rgba(0, 0, 0, 0.38);\n\n span {\n font-size: 12px;\n font-weight: 900;\n text-transform: uppercase;\n color: #aeefff;\n }\n\n b {\n margin-top: -2px;\n font-size: 28px;\n line-height: 30px;\n font-weight: 900;\n color: #ffffff;\n text-shadow: 0px 3px 6px rgba(0, 0, 0, 0.90);\n }\n}\n\n.cb-menu-actions {\n pointer-events: all;\n margin-top: auto;\n display: flex;\n flex-direction: row;\n justify-content: center;\n gap: 12px;\n width: auto;\n padding: 12px;\n background-color: rgba(4, 8, 20, 0.84);\n border: 1px solid rgba(255, 255, 255, 0.18);\n border-radius: 8px;\n box-shadow: 0px 16px 42px rgba(0, 0, 0, 0.50);\n\n .cb-btn {\n width: 178px;\n }\n}\n\n.cb-level-select {\n pointer-events: all;\n margin-top: 4px;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: center;\n gap: 12px;\n width: 398px;\n padding: 10px 12px;\n background-color: rgba(4, 8, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.16);\n border-radius: 8px;\n box-shadow: 0px 14px 36px rgba(0, 0, 0, 0.42);\n}\n\n.cb-level-nav {\n width: 48px;\n height: 46px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 28px;\n line-height: 28px;\n font-weight: 900;\n color: #07111a;\n background-color: #aeefff;\n border: 2px solid rgba(255, 255, 255, 0.82);\n border-radius: 8px;\n box-shadow: 0px 0px 16px rgba(92, 237, 255, 0.46);\n cursor: pointer;\n transition: all 0.10s ease-out;\n\n &:hover {\n transform: scale(1.04);\n border-color: #ffffff;\n }\n\n &:active {\n transform: scale(0.97);\n }\n\n &.disabled {\n color: rgba(255, 255, 255, 0.34);\n background-color: rgba(255, 255, 255, 0.06);\n border-color: rgba(255, 255, 255, 0.12);\n box-shadow: none;\n }\n}\n\n.cb-level-select-copy {\n width: 262px;\n display: flex;\n flex-direction: column;\n align-items: center;\n\n span {\n font-size: 12px;\n font-weight: 900;\n text-transform: uppercase;\n color: #aeefff;\n text-shadow: 0px 2px 3px rgba(0, 0, 0, 1);\n }\n\n b {\n margin-top: -1px;\n font-size: 19px;\n line-height: 22px;\n font-weight: 900;\n color: #ffffff;\n text-align: center;\n text-shadow: 0px 3px 5px rgba(0, 0, 0, 1);\n }\n}\n\n.cb-title-wrap {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 4px;\n}\n\n.cb-mode-tabs {\n display: flex;\n flex-direction: row;\n gap: 8px;\n margin: 6px 0px 2px;\n padding: 5px;\n background-color: rgba(4, 8, 20, 0.78);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-radius: 10px;\n}\n\n.cb-mode-tab {\n pointer-events: all;\n width: 150px;\n height: 42px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 17px;\n font-weight: 900;\n letter-spacing: 0px;\n color: rgba(255, 255, 255, 0.66);\n background-color: rgba(255, 255, 255, 0.05);\n border: 2px solid rgba(255, 255, 255, 0.10);\n border-radius: 8px;\n cursor: pointer;\n transition: all 0.10s ease-out;\n\n &:hover {\n color: #ffffff;\n border-color: rgba(92, 237, 255, 0.55);\n }\n\n &.active {\n color: #07111a;\n background-color: #aeefff;\n border-color: #ffffff;\n box-shadow: 0px 0px 16px rgba(92, 237, 255, 0.55);\n }\n}\n\n.cb-record-row {\n color: #07111a;\n background-color: #ffd23f;\n border-radius: 6px;\n\n span {\n color: #07111a;\n }\n\n b {\n color: #07111a;\n }\n}\n\n.cb-title {\n margin: 0px;\n font-size: 62px;\n line-height: 64px;\n font-weight: 900;\n letter-spacing: 0px;\n color: #ffffff;\n text-shadow:\n 0px 4px 0px rgba(20, 224, 255, 0.85),\n 0px 10px 0px rgba(199, 56, 255, 0.55),\n 0px 22px 42px rgba(0, 0, 0, 0.75);\n}\n\n.cb-tagline {\n margin: 0px;\n padding: 7px 12px;\n font-size: 16px;\n font-weight: 800;\n color: #f4fbff;\n background-color: rgba(4, 8, 20, 0.78);\n border: 1px solid rgba(255, 255, 255, 0.16);\n border-radius: 8px;\n text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.9);\n}\n\n.cb-level-brief {\n max-width: 520px;\n margin: 0px;\n padding: 8px 12px;\n font-size: 15px;\n line-height: 19px;\n font-weight: 800;\n color: rgba(255, 255, 255, 0.88);\n background-color: rgba(4, 8, 20, 0.66);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 8px;\n text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.95);\n}\n\n.cb-level-objective {\n max-width: 520px;\n margin: 0px;\n padding: 7px 12px;\n font-size: 15px;\n line-height: 18px;\n font-weight: 900;\n color: #07111a;\n background-color: #aeefff;\n border: 1px solid rgba(255, 255, 255, 0.85);\n border-radius: 8px;\n box-shadow: 0px 0px 18px rgba(92, 237, 255, 0.38);\n}\n\n.cb-level-rail {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 7px;\n margin-top: 4px;\n padding: 6px 8px;\n background-color: rgba(4, 8, 20, 0.58);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 8px;\n}\n\n.cb-level-pip {\n min-width: 28px;\n height: 28px;\n padding: 0px 7px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n font-weight: 900;\n color: rgba(255, 255, 255, 0.54);\n background-color: rgba(255, 255, 255, 0.08);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-radius: 8px;\n text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.90);\n\n &.complete {\n color: #04120b;\n background-color: rgba(66, 247, 123, 0.92);\n border-color: rgba(196, 255, 214, 0.95);\n text-shadow: none;\n }\n\n &.active {\n color: #07111a;\n background-color: #aeefff;\n border-color: #ffffff;\n box-shadow: 0px 0px 18px rgba(92, 237, 255, 0.80);\n text-shadow: none;\n }\n\n &.locked {\n color: rgba(255, 255, 255, 0.36);\n background-color: rgba(255, 255, 255, 0.04);\n border-color: rgba(255, 255, 255, 0.08);\n }\n}\n\n.cb-buttons {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n width: 320px;\n}\n\n.cb-btn {\n pointer-events: all;\n width: 100%;\n height: 58px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 22px;\n font-weight: 900;\n letter-spacing: 0px;\n color: #ffffff;\n background-color: rgba(11, 16, 32, 0.90);\n border: 2px solid rgba(255, 255, 255, 0.22);\n border-radius: 8px;\n box-shadow: 0px 12px 28px rgba(0, 0, 0, 0.40);\n transition: all 0.10s ease-out;\n cursor: pointer;\n\n &:hover {\n background-color: rgba(25, 36, 64, 0.96);\n border-color: rgba(255, 255, 255, 0.70);\n transform: scale(1.035);\n }\n\n &:active {\n transform: scale(0.98);\n }\n}\n\n.cb-primary {\n color: #04120b;\n background-color: #42f77b;\n border-color: #c4ffd6;\n text-shadow: none;\n\n &:hover {\n background-color: #6dff98;\n border-color: #ffffff;\n }\n}\n\n.cb-disabled {\n color: rgba(255, 255, 255, 0.42);\n background-color: rgba(255, 255, 255, 0.07);\n border-color: rgba(255, 255, 255, 0.14);\n box-shadow: none;\n\n &:hover {\n background-color: rgba(255, 255, 255, 0.08);\n border-color: rgba(255, 255, 255, 0.18);\n transform: none;\n }\n}\n\n.cb-hint {\n position: absolute;\n bottom: 30px;\n left: 0px;\n right: 0px;\n align-self: center;\n width: 620px;\n margin: 0px auto;\n padding: 11px 18px;\n justify-content: center;\n text-align: center;\n font-size: 16px;\n font-weight: 800;\n color: #f4fbff;\n background-color: rgba(4, 8, 20, 0.82);\n border: 1px solid rgba(92, 237, 255, 0.45);\n border-radius: 10px;\n box-shadow: 0px 10px 28px rgba(0, 0, 0, 0.50);\n text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.95);\n}\n\n.cb-hint-stuck {\n color: #07111a;\n background-color: #aeefff;\n border-color: #ffffff;\n box-shadow: 0px 0px 22px rgba(92, 237, 255, 0.55);\n text-shadow: none;\n}\n\n.cb-panel {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 22px;\n padding: 38px 48px;\n min-width: 380px;\n background-color: rgba(5, 8, 22, 0.94);\n border: 2px solid rgba(92, 237, 255, 0.42);\n border-radius: 8px;\n box-shadow: 0px 22px 70px rgba(0, 0, 0, 0.72);\n}\n\n.cb-settings-screen {\n z-index: 50;\n background-color: rgba(3, 5, 14, 0.94);\n}\n\n.cb-settings-panel {\n z-index: 51;\n min-width: 420px;\n background-color: rgba(5, 8, 22, 0.98);\n}\n\n.cb-settings-panel .cb-panel-title {\n font-size: 40px;\n}\n\n.cb-setting-row {\n pointer-events: all;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n width: 100%;\n min-height: 52px;\n padding: 10px 14px;\n color: rgba(255, 255, 255, 0.82);\n background-color: rgba(255, 255, 255, 0.06);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 6px;\n font-size: 17px;\n font-weight: 900;\n cursor: pointer;\n\n b {\n color: #42f77b;\n font-size: 18px;\n }\n\n &:hover {\n background-color: rgba(66, 247, 123, 0.16);\n border-color: rgba(66, 247, 123, 0.72);\n }\n\n &.focused {\n color: #ffffff;\n background-color: rgba(92, 237, 255, 0.18);\n border-color: rgba(92, 237, 255, 0.92);\n box-shadow: 0px 0px 18px rgba(92, 237, 255, 0.42);\n }\n}\n\n.cb-panel-title {\n margin: 0px;\n font-size: 46px;\n font-weight: 900;\n color: #ffffff;\n text-shadow: 0px 5px 18px rgba(78, 226, 255, 0.65);\n}\n\n.cb-readout {\n display: flex;\n flex-direction: column;\n gap: 8px;\n width: 100%;\n}\n\n.cb-readout-row {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n width: 100%;\n padding: 9px 12px;\n font-size: 17px;\n font-weight: 800;\n color: rgba(255, 255, 255, 0.68);\n background-color: rgba(255, 255, 255, 0.06);\n border-radius: 6px;\n\n b {\n color: #ffffff;\n font-weight: 900;\n }\n}\n\n.cb-hud {\n position: absolute;\n top: 20px;\n left: 20px;\n width: 300px;\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n pointer-events: none;\n}\n\n.cb-stat,\n.cb-status {\n display: flex;\n flex-direction: column;\n justify-content: center;\n width: 100%;\n min-height: 58px;\n padding: 8px 14px;\n background-color: rgba(4, 7, 18, 0.92);\n border: 2px solid rgba(255, 255, 255, 0.24);\n border-radius: 8px;\n box-shadow:\n 0px 10px 24px rgba(0, 0, 0, 0.45),\n inset 0px 0px 18px rgba(58, 226, 255, 0.10);\n}\n\n.cb-stat-label,\n.cb-status-label {\n font-size: 14px;\n font-weight: 900;\n text-transform: uppercase;\n color: #aeefff;\n text-shadow: 0px 2px 3px rgba(0, 0, 0, 1);\n}\n\n.cb-stat-value {\n margin-top: -2px;\n font-size: 32px;\n line-height: 32px;\n font-weight: 900;\n color: #ffffff;\n text-shadow: 0px 3px 5px rgba(0, 0, 0, 1);\n}\n\n.cb-stat-subtitle {\n margin-top: 2px;\n font-size: 14px;\n line-height: 16px;\n font-weight: 900;\n color: rgba(255, 255, 255, 0.82);\n text-shadow: 0px 2px 4px rgba(0, 0, 0, 1);\n}\n\n.cb-status {\n min-width: 0px;\n}\n\n.cb-progress {\n width: 100%;\n min-height: 62px;\n padding: 9px 14px;\n display: flex;\n flex-direction: column;\n justify-content: center;\n background-color: rgba(4, 7, 18, 0.92);\n border: 2px solid rgba(255, 255, 255, 0.24);\n border-radius: 8px;\n box-shadow:\n 0px 10px 24px rgba(0, 0, 0, 0.45),\n inset 0px 0px 18px rgba(66, 247, 123, 0.10);\n}\n\n.cb-progress-copy {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n\n span {\n font-size: 14px;\n font-weight: 900;\n text-transform: uppercase;\n color: #aeefff;\n text-shadow: 0px 2px 3px rgba(0, 0, 0, 1);\n }\n\n b {\n font-size: 18px;\n font-weight: 900;\n color: #ffffff;\n text-shadow: 0px 2px 3px rgba(0, 0, 0, 1);\n }\n}\n\n.cb-progress-track {\n width: 100%;\n height: 10px;\n margin-top: 7px;\n background-color: rgba(255, 255, 255, 0.10);\n border-radius: 5px;\n overflow: hidden;\n}\n\n.cb-progress-fill {\n height: 100%;\n min-width: 3px;\n background-color: #42f77b;\n border-radius: 5px;\n box-shadow: 0px 0px 18px rgba(66, 247, 123, 0.75);\n}\n\n.cb-objective {\n box-shadow:\n 0px 10px 24px rgba(0, 0, 0, 0.45),\n inset 0px 0px 18px rgba(92, 237, 255, 0.16);\n}\n\n.cb-objective-name {\n margin-top: 2px;\n font-size: 13px;\n line-height: 15px;\n font-weight: 900;\n color: rgba(255, 255, 255, 0.86);\n text-shadow: 0px 2px 3px rgba(0, 0, 0, 1);\n}\n\n.cb-objective-fill {\n background-color: #aeefff;\n box-shadow: 0px 0px 18px rgba(92, 237, 255, 0.82);\n}\n\n.cb-status-value {\n margin-top: 2px;\n font-size: 20px;\n line-height: 22px;\n font-weight: 900;\n color: #ffffff;\n text-shadow: 0px 3px 5px rgba(0, 0, 0, 1);\n}\n\n.cb-pause-btn {\n position: absolute;\n top: 20px;\n right: 20px;\n pointer-events: all;\n width: 68px;\n min-height: 68px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 24px;\n font-weight: 900;\n color: #ffffff;\n background-color: rgba(4, 7, 18, 0.92);\n border: 2px solid rgba(255, 255, 255, 0.30);\n border-radius: 8px;\n box-shadow: 0px 10px 24px rgba(0, 0, 0, 0.45);\n cursor: pointer;\n transition: all 0.10s ease-out;\n\n &:hover {\n color: #04120b;\n background-color: #42f77b;\n border-color: #ffffff;\n transform: scale(1.04);\n }\n\n &:active {\n transform: scale(0.97);\n }\n}\n\n/* -------- bug reporting -------- */\n\n.cb-report-panel {\n min-width: 520px;\n}\n\n.cb-report-intro {\n margin: 0px;\n max-width: 460px;\n font-size: 15px;\n line-height: 19px;\n font-weight: 700;\n text-align: center;\n color: rgba(255, 255, 255, 0.82);\n text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.9);\n}\n\n.cb-report-input {\n width: 460px;\n height: 150px;\n padding: 12px 14px;\n font-size: 16px;\n font-weight: 600;\n color: #ffffff;\n background-color: rgba(255, 255, 255, 0.06);\n border: 2px solid rgba(92, 237, 255, 0.45);\n border-radius: 8px;\n\n &:focus {\n border-color: #aeefff;\n background-color: rgba(255, 255, 255, 0.10);\n }\n}\n\n.cb-report-feedback {\n width: 460px;\n padding: 8px 12px;\n font-size: 15px;\n font-weight: 800;\n text-align: center;\n color: #07120b;\n background-color: #42f77b;\n border-radius: 8px;\n}\n\n.cb-manager-btn {\n color: #07111a;\n background-color: #ffd23f;\n border-color: #ffe98a;\n\n &:hover {\n background-color: #ffe06a;\n border-color: #ffffff;\n }\n}\n\n.cb-manager-panel {\n min-width: 620px;\n max-width: 760px;\n}\n\n.cb-manager-list {\n display: flex;\n flex-direction: column;\n gap: 10px;\n width: 660px;\n max-height: 460px;\n overflow-y: scroll;\n padding-right: 4px;\n}\n\n.cb-manager-empty {\n padding: 28px;\n font-size: 17px;\n font-weight: 800;\n text-align: center;\n color: rgba(255, 255, 255, 0.6);\n}\n\n.cb-report-row {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 14px;\n background-color: rgba(255, 255, 255, 0.06);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-left-width: 4px;\n border-left-color: #ff7a59;\n border-radius: 8px;\n\n &.resolved {\n border-left-color: #42f77b;\n opacity: 0.7;\n }\n}\n\n.cb-report-head {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 10px;\n}\n\n.cb-report-when {\n font-size: 13px;\n font-weight: 900;\n color: #aeefff;\n}\n\n.cb-report-meta {\n flex-grow: 1;\n font-size: 13px;\n font-weight: 700;\n color: rgba(255, 255, 255, 0.66);\n}\n\n.cb-report-tag {\n padding: 2px 8px;\n font-size: 12px;\n font-weight: 900;\n color: #07120b;\n background-color: #42f77b;\n border-radius: 10px;\n}\n\n.cb-report-msg {\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n color: #ffffff;\n white-space: pre-wrap;\n}\n\n.cb-report-actions {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n gap: 8px;\n margin-top: 2px;\n}\n\n.cb-mini-btn {\n pointer-events: all;\n padding: 6px 14px;\n font-size: 14px;\n font-weight: 900;\n color: #07111a;\n background-color: #aeefff;\n border: 1px solid rgba(255, 255, 255, 0.7);\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.10s ease-out;\n\n &:hover {\n background-color: #ffffff;\n transform: scale(1.04);\n }\n}\n\n.cb-mini-danger {\n color: #ffffff;\n background-color: rgba(255, 90, 70, 0.92);\n border-color: rgba(255, 150, 130, 0.9);\n\n &:hover {\n background-color: #ff5a46;\n }\n}\n"
},
{
"Ident": "verarp.colour_break",
"Path": "ColourBreakProgressStore.cs",
"FileName": "ColourBreakProgressStore.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "using Sandbox;\nusing System;\n\npublic static class ColourBreakProgressStore\n{\n\tconst string ProgressCookieKey = \"colourbreak.progress.v1\";\n\n\tpublic static ColourBreakSaveData Load()\n\t{\n\t\tColourBreakSaveData data;\n\t\ttry\n\t\t{\n\t\t\tdata = Game.Cookies.Get( ProgressCookieKey, new ColourBreakSaveData() ) ?? new ColourBreakSaveData();\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"Could not load Colour Break progress: {e.Message}\" );\n\t\t\tdata = new ColourBreakSaveData();\n\t\t}\n\n\t\tNormalize( data );\n\t\treturn data;\n\t}\n\n\tpublic static void Save( ColourBreakSaveData data )\n\t{\n\t\tNormalize( data );\n\n\t\ttry\n\t\t{\n\t\t\tGame.Cookies.Set( ProgressCookieKey, data );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"Could not save Colour Break progress: {e.Message}\" );\n\t\t}\n\t}\n\n\tpublic static int GetBestStars( ColourBreakSaveData data, int levelIndex )\n\t{\n\t\tif ( data?.BestStars == null )\n\t\t\treturn 0;\n\n\t\tif ( levelIndex < 0 || levelIndex >= data.BestStars.Length )\n\t\t\treturn 0;\n\n\t\treturn Math.Clamp( data.BestStars[levelIndex], 0, 3 );\n\t}\n\n\tpublic static bool RecordEndlessScore( ColourBreakSaveData data, int score )\n\t{\n\t\tNormalize( data );\n\n\t\tif ( score <= data.BestEndlessScore )\n\t\t\treturn false;\n\n\t\tdata.BestEndlessScore = score;\n\t\tSave( data );\n\t\treturn true;\n\t}\n\n\tpublic static void RecordWin( ColourBreakSaveData data, int levelIndex, int stars, bool hasNextLevel )\n\t{\n\t\tNormalize( data );\n\n\t\tif ( levelIndex >= 0 && levelIndex < data.BestStars.Length )\n\t\t\tdata.BestStars[levelIndex] = Math.Max( data.BestStars[levelIndex], Math.Clamp( stars, 1, 3 ) );\n\n\t\tif ( hasNextLevel )\n\t\t\tdata.HighestUnlockedLevel = Math.Max( data.HighestUnlockedLevel, levelIndex + 1 );\n\n\t\tSave( data );\n\t}\n\n\tstatic void Normalize( ColourBreakSaveData data )\n\t{\n\t\tif ( data == null )\n\t\t\treturn;\n\n\t\tdata.HighestUnlockedLevel = Math.Clamp( data.HighestUnlockedLevel, 1, ColourBreakLevels.Count );\n\n\t\tif ( data.BestEndlessScore < 0 )\n\t\t\tdata.BestEndlessScore = 0;\n\n\t\tif ( data.BestStars == null || data.BestStars.Length < ColourBreakLevels.Count + 1 )\n\t\t{\n\t\t\tvar old = data.BestStars;\n\t\t\tdata.BestStars = new int[ColourBreakLevels.Count + 1];\n\t\t\tif ( old != null )\n\t\t\t{\n\t\t\t\tint count = Math.Min( old.Length, data.BestStars.Length );\n\t\t\t\tfor ( int i = 0; i < count; i++ )\n\t\t\t\t\tdata.BestStars[i] = Math.Clamp( old[i], 0, 3 );\n\t\t\t}\n\t\t}\n\n\t\tfor ( int i = 0; i < data.BestStars.Length; i++ )\n\t\t\tdata.BestStars[i] = Math.Clamp( data.BestStars[i], 0, 3 );\n\t}\n}\n"
},
{
"Ident": "verarp.colour_break",
"Path": "styles/base/_splitcontainer.scss",
"FileName": "_splitcontainer.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "\r\n.splitcontainer\r\n{\r\n\tflex-grow: 1;\r\n\r\n\t> .split-left, > .split-right\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t}\r\n\r\n\t> .split-left\r\n\t{\r\n\t\theight: 100%;\r\n\t}\r\n\r\n\t> .splitter\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\twidth: 8px;\r\n\t\tcursor: ew-resize;\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t}\r\n\t}\r\n\r\n\t&.dragging > .splitter\r\n\t{\r\n\t}\r\n\r\n\t&.vertical\r\n\t{\r\n\t\tflex-direction: column;\r\n\r\n\t\t> .splitter\r\n\t\t{\r\n\t\t\theight: 8px;\r\n\t\t\twidth: 100%;\r\n\t\t\tcursor: ns-resize;\r\n\t\t}\r\n\t}\r\n}"
},
{
"Ident": "verarp.colour_break",
"Path": "styles/base/_navigator.scss",
"FileName": "_navigator.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": ".navigator-body\r\n{\r\n\t&.hidden\r\n\t{\r\n\t\tdisplay: none;\r\n\t}\r\n}\r\n"
},
{
"Ident": "verarp.colour_break",
"Path": "styles/form.scss",
"FileName": "form.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "\r\n$form-control-height: 28px !default;\r\n\r\n@import \"form/_checkbox.scss\";\r\n@import \"form/_switch.scss\";\r\n@import \"form/_dropdown.scss\";\r\n@import \"form/_coloreditor.scss\";\r\n@import \"form/_colorproperty.scss\";\r\n\r\n.form\r\n{\r\n\tflex-direction: column;\r\n\talign-items: stretch;\r\n\tjustify-content: flex-start;\r\n\toverflow: scroll;\r\n}\r\n\r\n.field-group\r\n{\r\n\tflex-direction: column;\r\n\tflex-shrink: 0;\r\n}\r\n\r\n.field-header\r\n{\r\n\tflex-shrink: 0;\r\n}\r\n\r\n.field\r\n{\r\n\tcolor: white;\r\n\tfont-size: 14px;\r\n\tflex-shrink: 0;\r\n\tflex-grow: 0;\r\n\r\n\t> .label\r\n\t{\r\n\t\tflex-grow: 0;\r\n\t\tflex-shrink: 0;\r\n\t\tfont-weight: 600;\r\n\t\topacity: 0.4;\r\n\t\twidth: 20%;\r\n\t\tfont-size: 13px;\r\n\t}\r\n\r\n\t> .control\r\n\t{\r\n\t\tflex-shrink: 0;\r\n\t\tflex-grow: 1;\r\n\t\tflex-direction: column;\r\n\t}\r\n}\r\n\r\n.is-vertical > .field, .field.is-vertical\r\n{\r\n\tflex-direction: column;\r\n\r\n\t> .label\r\n\t{\r\n\t\twidth: auto;\r\n\t\theight: auto;\r\n\t}\r\n}"
},
{
"Ident": "verarp.colour_break",
"Path": "styles/base.scss",
"FileName": "base.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "@import \"base/_splitcontainer.scss\";\r\n@import \"base/_navigator.scss\";\r\n\r\nbutton\r\n{\r\n\tcursor: pointer;\r\n}\r\n\r\nIconPanel\r\n{\r\n\tfont-family: Material Icons;\r\n}\r\n\r\n.is-half\r\n{\r\n\twidth: 50%;\r\n}\r\n\r\n.is-third\r\n{\r\n\twidth: 33%;\r\n}\r\n\r\n.is-quarter\r\n{\r\n\twidth: 25%;\r\n}\r\n\r\nbutton.has-subtitle\r\n{\r\n\tposition: relative;\r\n\tflex-direction: column;\r\n\tjustify-content: flex-start;\r\n\talign-items: flex-start;\r\n\tpadding-left: 40px; // icon space\r\n\r\n\t.iconpanel\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tleft: 5px;\r\n\t\ttop: 0;\r\n\t\tbottom: 0;\r\n\t\talign-items: center;\r\n\t}\r\n\r\n\t.button-label\r\n\t{\r\n\t\tfont-weight: bold;\r\n\t}\r\n\r\n\t.button-subtitle\r\n\t{\r\n\t\tfont-size: 12px;\r\n\t\topacity: 0.5;\r\n\t\tmix-blend-mode: lighten;\r\n\t}\r\n}"
},
{
"Ident": "verarp.colour_break",
"Path": "ui/controls/enumcontrol.cs.scss",
"FileName": "enumcontrol.cs.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "EnumControl\r\n{\r\n\tgap: 2px;\r\n\tflex-grow: 1;\r\n}\r\n\r\nEnumControl DropDown,\r\nEnumControl ButtonGroup\r\n{\r\n\tborder-radius: 8px;\r\n\tbackground-color: #000a;\r\n\tflex-grow: 1;\r\n}\r\n\r\nEnumControl DropDown\r\n{\r\n\tflex-grow: 1;\r\n\tmin-height: 32px;\r\n}\r\n\r\nEnumControl ButtonGroup\r\n{\r\n\tborder-radius: 12px;\r\n\toverflow: hidden;\r\n\tmin-height: 32px;\r\n\r\n\tButton\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tgap: 4px;\r\n\t\tcolor: #aaa;\r\n\t\tfont-size: 1rem;\r\n\t\tcursor: pointer;\r\n\r\n\t\t.icon\r\n\t\t{\r\n\t\t\tcolor: #08f;\r\n\t\t}\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\tcolor: #ddd;\r\n\r\n\t\t\t.icon\r\n\t\t\t{\r\n\t\t\t\tcolor: #3af;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t&:active\r\n\t\t{\r\n\t\t\tbackground-color: #04a;\r\n\t\t\tcolor: white;\r\n\t\t\ttransform: translateX( 1px ) translateY( 1px );\r\n\r\n\t\t\t.icon\r\n\t\t\t{\r\n\t\t\t\tcolor: #fff;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t&.active\r\n\t\t{\r\n\t\t\tbackground-color: #08f;\r\n\t\t\tcolor: white;\r\n\t\t\tpointer-events: none;\r\n\r\n\t\t\t.icon\r\n\t\t\t{\r\n\t\t\t\tcolor: #fff;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}"
},
{
"Ident": "verarp.colour_break",
"Path": "ColourBreakSaveData.cs",
"FileName": "ColourBreakSaveData.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "public sealed class ColourBreakSaveData\n{\n\tpublic int HighestUnlockedLevel { get; set; } = 1;\n\tpublic int[] BestStars { get; set; } = new int[ColourBreakLevels.Count + 1];\n\tpublic int BestEndlessScore { get; set; }\n}\n"
},
{
"Ident": "verarp.colour_break",
"Path": "ui/components/packageflairbar.razor.scss",
"FileName": "packageflairbar.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "PackageFlairBar\r\n{\r\n\tposition: absolute;\r\n\ttop: 6px;\r\n\tleft: 6px;\r\n\tz-index: 2;\r\n\tflex-direction: row;\r\n\tgap: 5px;\r\n\tpointer-events: none;\r\n\r\n\t.flair\r\n\t{\r\n\t\tposition: relative;\r\n\t\talign-items: center;\r\n\t\tjustify-content: center;\r\n\t\twidth: 22px;\r\n\t\theight: 22px;\r\n\t\tpointer-events: all;\r\n\t\tborder-radius: 4px;\r\n\t\tcolor: white;\r\n\t\tborder: 1px solid rgba( white, 0.1 );\r\n\t\toutline: 1px solid #0006;\r\n\t\topacity: 0.7;\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\topacity: 1;\r\n\t\t}\r\n\r\n\t\ticon\r\n\t\t{\r\n\t\t\tfont-size: 15px;\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "verarp.colour_break",
"Path": "ColourBreakBugReportStore.cs",
"FileName": "ColourBreakBugReportStore.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "using Sandbox;\nusing System;\nusing System.Linq;\nusing System.Collections.Generic;\n\n// Local persistence for bug reports, stored in a cookie like the progress store.\n// No network calls \u2014 the dev reads them in-game via the hidden manager.\npublic static class ColourBreakBugReportStore\n{\n\tconst string CookieKey = \"colourbreak.bugreports.v1\";\n\tconst int MaxReports = 200;\n\tconst int MaxMessageLength = 600;\n\n\tpublic static ColourBreakBugReportLog Load()\n\t{\n\t\ttry\n\t\t{\n\t\t\treturn Game.Cookies.Get( CookieKey, new ColourBreakBugReportLog() ) ?? new ColourBreakBugReportLog();\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"Could not load bug reports: {e.Message}\" );\n\t\t\treturn new ColourBreakBugReportLog();\n\t\t}\n\t}\n\n\tpublic static void Save( ColourBreakBugReportLog log )\n\t{\n\t\tif ( log == null ) return;\n\t\ttry\n\t\t{\n\t\t\tGame.Cookies.Set( CookieKey, log );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"Could not save bug reports: {e.Message}\" );\n\t\t}\n\t}\n\n\tpublic static List<ColourBreakBugReport> GetAll()\n\t{\n\t\treturn Load().Reports ?? new List<ColourBreakBugReport>();\n\t}\n\n\tpublic static int OpenCount()\n\t{\n\t\treturn GetAll().Count( r => r != null && !r.Resolved );\n\t}\n\n\tpublic static bool Add( ColourBreakBugReport report )\n\t{\n\t\tif ( report == null )\n\t\t\treturn false;\n\n\t\treport.Message = (report.Message ?? \"\").Trim();\n\t\tif ( string.IsNullOrWhiteSpace( report.Message ) )\n\t\t\treturn false;\n\n\t\tif ( report.Message.Length > MaxMessageLength )\n\t\t\treport.Message = report.Message.Substring( 0, MaxMessageLength );\n\n\t\tvar log = Load();\n\t\tlog.Reports ??= new List<ColourBreakBugReport>();\n\t\tlog.Reports.Insert( 0, report ); // newest first\n\t\tif ( log.Reports.Count > MaxReports )\n\t\t\tlog.Reports.RemoveRange( MaxReports, log.Reports.Count - MaxReports );\n\n\t\tSave( log );\n\t\treturn true;\n\t}\n\n\tpublic static void SetResolved( string id, bool resolved )\n\t{\n\t\tvar log = Load();\n\t\tvar r = log.Reports?.FirstOrDefault( x => x != null && x.Id == id );\n\t\tif ( r == null ) return;\n\t\tr.Resolved = resolved;\n\t\tSave( log );\n\t}\n\n\tpublic static void Delete( string id )\n\t{\n\t\tvar log = Load();\n\t\tlog.Reports?.RemoveAll( x => x == null || x.Id == id );\n\t\tSave( log );\n\t}\n\n\tpublic static void Clear()\n\t{\n\t\tSave( new ColourBreakBugReportLog() );\n\t}\n}\n"
},
{
"Ident": "verarp.colour_break",
"Path": "ColourBreakGame.cs",
"FileName": "ColourBreakGame.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "using Sandbox;\nusing System;\nusing System.Collections.Generic;\n\npublic sealed class ColourBreakGame : Component\n{\n\tconst int Empty = -1;\n\n\t[Property] public int Width { get; set; } = 8;\n\t[Property] public int Height { get; set; } = 8;\n\t[Property] public float CellSpacing { get; set; } = 48f;\n\t[Property] public float CandyScale { get; set; } = 0.50f;\n\t[Property] public Vector3 BoardOrigin { get; set; } = Vector3.Zero;\n\t[Property] public Model CandyModel { get; set; }\n\t[Property] public int CandyTypeCount { get; set; } = 6;\n\t[Property] public bool AutoFrameMainCamera { get; set; } = true;\n\t[Property] public int LevelIndex { get; set; } = 1;\n\t[Property] public int TargetScore { get; set; } = 850;\n\t[Property] public int MoveLimit { get; set; } = 24;\n\t[Property] public bool AudioEnabled { get; set; } = true;\n\t[Property] public bool EffectsEnabled { get; set; } = true;\n\t[Property] public bool CameraShakeEnabled { get; set; } = true;\n\t[Property] public bool ColourBlindMode { get; set; }\n\t[Property] public string StartSound { get; set; } = \"sounds/colourbreak_start.sound\";\n\t[Property] public string SwapSound { get; set; } = \"sounds/colourbreak_swap.sound\";\n\t[Property] public string ClearSound { get; set; } = \"sounds/colourbreak_clear.sound\";\n\t[Property] public string ComboSound { get; set; } = \"sounds/colourbreak_combo.sound\";\n\t[Property] public string DeniedSound { get; set; } = \"sounds/colourbreak_denied.sound\";\n\t[Property] public bool MusicEnabled { get; set; } = true;\n\t[Property] public string MusicSound { get; set; } = \"sounds/colourbreak_music.sound\";\n\t[Property, Range( 0f, 1f )] public float MusicVolume { get; set; } = 0.42f;\n\t[Property] public GameMode Mode { get; set; } = GameMode.Levels;\n\n\t// Endless uses the full palette and the largest shape set available.\n\tconst int EndlessColourCount = 6;\n\n\tModel _sphereModel;\n\tModel _boxModel;\n\n\tModel ModelForType( int t ) => _sphereModel ?? _boxModel;\n\tColor PieceColor( int type ) => (ColourBlindMode ? ColourBlindPalette : Palette)[Math.Clamp( type, 0, Palette.Length - 1 )];\n\tVector3 PieceShape( int type ) => (ColourBlindMode ? ColourBlindShapes : Shapes)[Math.Clamp( type, 0, Shapes.Length - 1 )];\n\n\t// Distinct saturated colours that read quickly under motion and bloom.\n\treadonly Color[] Palette = new[]\n\t{\n\t\tnew Color( 1.00f, 0.05f, 0.15f ), // bright red\n\t\tnew Color( 1.00f, 0.50f, 0.00f ), // bright orange\n\t\tnew Color( 1.00f, 0.95f, 0.00f ), // bright yellow\n\t\tnew Color( 0.15f, 0.90f, 0.15f ), // bright green\n\t\tnew Color( 0.05f, 0.55f, 1.00f ), // bright blue\n\t\tnew Color( 0.80f, 0.15f, 1.00f ), // bright purple\n\t};\n\n\t// Okabe-Ito-derived hues, pushed brighter/more saturated so they stay distinct\n\t// under bloom. Backed up by the per-type shape symbols in ApplyColourBlindSymbol.\n\treadonly Color[] ColourBlindPalette = new[]\n\t{\n\t\tnew Color( 1.00f, 0.55f, 0.00f ), // vivid orange\n\t\tnew Color( 0.30f, 0.78f, 1.00f ), // sky blue\n\t\tnew Color( 0.00f, 0.78f, 0.50f ), // bluish green\n\t\tnew Color( 1.00f, 0.95f, 0.20f ), // yellow\n\t\tnew Color( 0.05f, 0.35f, 0.95f ), // deep blue\n\t\tnew Color( 0.95f, 0.40f, 0.70f ), // reddish purple / pink\n\t};\n\n\treadonly string[] ColourNames =\n\t{\n\t\t\"Ruby\",\n\t\t\"Amber\",\n\t\t\"Gold\",\n\t\t\"Lime\",\n\t\t\"Azure\",\n\t\t\"Violet\",\n\t};\n\n\treadonly Vector3[] Shapes = new[]\n\t{\n\t\tnew Vector3( 1.00f, 1.00f, 1.00f ),\n\t\tnew Vector3( 1.18f, 0.92f, 1.08f ),\n\t\tnew Vector3( 0.92f, 1.08f, 1.24f ),\n\t\tnew Vector3( 1.08f, 1.00f, 0.86f ),\n\t\tnew Vector3( 0.94f, 1.06f, 1.14f ),\n\t\tnew Vector3( 1.10f, 0.90f, 0.98f ),\n\t\tnew Vector3( 1.00f, 1.00f, 1.00f ),\n\t};\n\n\treadonly Vector3[] ColourBlindShapes = new[]\n\t{\n\t\tnew Vector3( 0.78f, 1.00f, 1.42f ),\n\t\tnew Vector3( 1.42f, 0.92f, 0.78f ),\n\t\tnew Vector3( 1.00f, 1.00f, 1.00f ),\n\t\tnew Vector3( 1.28f, 0.92f, 1.28f ),\n\t\tnew Vector3( 0.78f, 1.08f, 0.78f ),\n\t\tnew Vector3( 1.48f, 0.86f, 1.08f ),\n\t\tnew Vector3( 1.00f, 1.00f, 1.00f ),\n\t};\n\n\treadonly Dictionary<string, bool> _prevKeys = new();\n\n\tint[,] _types;\n\tSpecialKind[,] _specials;\n\tCandyVisual[,] _visuals;\n\treadonly List<GameObject> _decor = new();\n\n\tint _cursorX;\n\tint _cursorY;\n\tbool _hasSelection;\n\tint _selectedX;\n\tint _selectedY;\n\n\tint _score;\n\tint _moves;\n\tint _objectiveProgress;\n\tColourBreakSaveData _saveData = new();\n\tbool _prevMouseLeft;\n\tfloat _controllerMoveCooldown;\n\n\tbool _dragging;\n\tint _dragStartX;\n\tint _dragStartY;\n\tbool _dragSwapped;\n\n\t// Idle hint: after a stretch of inactivity, gently pulse a valid move so the\n\t// player is never truly stuck staring at the board.\n\tfloat _idleTime;\n\tbool _hasHint;\n\tint _hintAX, _hintAY, _hintBX, _hintBY;\n\tconst float HintDelay = 5.0f;\n\n\t// Tracks whether the player has ever made a swap this session, so the\n\t// how-to-play prompt can fade out once they clearly understand the controls.\n\tbool _hasEverSwapped;\n\n\t// High-level game state driving the menus\n\tpublic enum GameState { Menu, Playing, Paused, Won, Lost }\n\tpublic enum GameMode { Levels, Endless }\n\tpublic GameState CurrentState { get; private set; } = GameState.Menu;\n\tpublic bool IsEndless => Mode == GameMode.Endless;\n\tenum SpecialKind { None, RowBlast, ColumnBlast, Bomb }\n\n\t// Exposed for the UI / HUD\n\tpublic int Score => _score;\n\tpublic int Moves => _moves;\n\tpublic int MovesLeft => Math.Max( 0, MoveLimit - _moves );\n\tpublic int GoalScore => TargetScore;\n\tpublic int Level => LevelIndex;\n\tpublic int ColourCount => CandyTypeCount;\n\tColourBreakLevelDefinition CurrentLevelDefinition => ColourBreakLevels.Get( LevelIndex );\n\tpublic string LevelName => CurrentLevelDefinition.Name;\n\tpublic string LevelBrief => CurrentLevelDefinition.Brief;\n\tpublic int LevelCount => ColourBreakLevels.Count;\n\tpublic bool HasNextLevel => LevelIndex < ColourBreakLevels.Count;\n\tpublic int HighestUnlockedLevel => Math.Clamp( _saveData?.HighestUnlockedLevel ?? 1, 1, LevelCount );\n\tpublic int BestStars => ColourBreakProgressStore.GetBestStars( _saveData, LevelIndex );\n\tpublic int TotalStars\n\t{\n\t\tget\n\t\t{\n\t\t\tint total = 0;\n\t\t\tfor ( int i = 1; i <= LevelCount; i++ )\n\t\t\t\ttotal += ColourBreakProgressStore.GetBestStars( _saveData, i );\n\t\t\treturn total;\n\t\t}\n\t}\n\tpublic int MaxStars => LevelCount * 3;\n\tpublic int EndlessBest => Math.Max( 0, _saveData?.BestEndlessScore ?? 0 );\n\tpublic bool EndlessRecordSet { get; private set; }\n\tpublic bool SelectedLevelLocked => LevelIndex > HighestUnlockedLevel;\n\tpublic bool CanPlaySelectedLevel => !SelectedLevelLocked;\n\tpublic bool CanSelectPreviousLevel => CurrentState == GameState.Menu && LevelIndex > 1;\n\tpublic bool CanSelectNextLevel => CurrentState == GameState.Menu && LevelIndex < LevelCount;\n\tpublic bool HasSecondaryObjective => CurrentLevelDefinition.ObjectiveKind != ColourBreakObjectiveKind.Score && CurrentLevelDefinition.ObjectiveTarget > 0;\n\tpublic int ObjectiveProgress => HasSecondaryObjective ? Math.Min( _objectiveProgress, CurrentLevelDefinition.ObjectiveTarget ) : 0;\n\tpublic int ObjectiveTarget => HasSecondaryObjective ? CurrentLevelDefinition.ObjectiveTarget : 0;\n\tpublic string ObjectiveText => ObjectiveLabel();\n\tpublic string ObjectiveProgressText => HasSecondaryObjective\n\t\t? $\"{ObjectiveProgress} / {ObjectiveTarget}\"\n\t\t: \"Score target\";\n\tpublic bool ObjectiveComplete => !HasSecondaryObjective || _objectiveProgress >= CurrentLevelDefinition.ObjectiveTarget;\n\tpublic int CompletionStars\n\t{\n\t\tget\n\t\t{\n\t\t\tif ( CurrentState != GameState.Won )\n\t\t\t\treturn 0;\n\n\t\t\tint stars = 1;\n\t\t\tif ( MovesLeft >= Math.Max( 3, MoveLimit / 4 ) )\n\t\t\t\tstars++;\n\t\t\tif ( _score >= MathF.Ceiling( TargetScore * 1.25f ) )\n\t\t\t\tstars++;\n\n\t\t\treturn Math.Clamp( stars, 1, 3 );\n\t\t}\n\t}\n\tpublic string CompletionRank => CompletionStars switch\n\t{\n\t\t3 => \"Perfect Break\",\n\t\t2 => \"Clean Break\",\n\t\t1 => \"Level Clear\",\n\t\t_ => \"Keep Going\"\n\t};\n\tpublic int ComboCount => _comboCount;\n\tpublic bool HasLegalMove => HasPossibleMove();\n\tpublic bool MusicIsPlaying => _music.IsValid() && _music.IsPlaying;\n\tpublic int ActiveSpecialCount => CountSpecials();\n\t// Drives the one-time how-to-play prompt and the \"stuck?\" hint banner in the HUD.\n\tpublic bool ShowHowToPlay => CurrentState == GameState.Playing && !_hasEverSwapped;\n\tpublic bool HintActive => _hasHint && CurrentState == GameState.Playing;\n\tpublic string StatusText => _phase switch\n\t{\n\t\tPhase.SwapForward => \"Swapping\",\n\t\tPhase.SwapBack => \"Try another move\",\n\t\tPhase.Clearing => _comboCount > 1 ? $\"Combo x{_comboCount}\" : \"Colour break\",\n\t\tPhase.Collapsing => \"Dropping\",\n\t\tPhase.Refilling => \"Refilling\",\n\t\tPhase.Pause => _comboCount > 1 ? $\"Chain x{_comboCount}\" : \"Scoring\",\n\t\t_ when _hasSelection => \"Pick a neighbour\",\n\t\t_ => CurrentState switch\n\t\t{\n\t\t\tGameState.Won => \"Level complete\",\n\t\t\tGameState.Lost => IsEndless ? \"No moves left\" : \"Out of moves\",\n\t\t\tGameState.Playing => IsEndless ? \"Keep the chain alive\" : \"Find a colour chain\",\n\t\t\t_ => \"Ready\"\n\t\t}\n\t};\n\n\t// Resolve / animation state machine\n\tenum Phase { Idle, SwapForward, ValidateAndClear, Clearing, Collapsing, Refilling, Pause, SwapBack }\n\tPhase _phase = Phase.Idle;\n\tfloat _phaseTimer;\n\tint _swapAX, _swapAY, _swapBX, _swapBY;\n\tint _comboCount;\n\n\t// Camera punch\n\tCameraComponent _activeCamera;\n\tfloat _baseFov = 60f;\n\tfloat _fovPunch;\n\tfloat _shakeAmount;\n\tVector3 _baseCameraPos;\n\n\t// Orbiting accent lights\n\tGameObject _orbitLightL;\n\tGameObject _orbitLightR;\n\n\t// Color-shifting tray\n\tModelRenderer _trayInnerRenderer;\n\tColor _trayBaseColor = new Color( 0.62f, 0.70f, 0.92f );\n\n\tconst float SwapDuration = 0.18f;\n\tconst float ClearDuration = 0.28f;\n\tconst float FallSpeed = 1100f; // units per second\n\tconst float CascadePause = 0.05f;\n\n\tsealed class CandyVisual\n\t{\n\t\tpublic GameObject Go;\n\t\tpublic ModelRenderer Renderer;\n\t\tpublic GameObject Shine;\n\t\tpublic ModelRenderer ShineRenderer;\n\t\tpublic GameObject Rim;\n\t\tpublic ModelRenderer RimRenderer;\n\t\tpublic GameObject Accent;\n\t\tpublic ModelRenderer AccentRenderer;\n\t\tpublic GameObject SymbolA;\n\t\tpublic ModelRenderer SymbolARenderer;\n\t\tpublic GameObject SymbolB;\n\t\tpublic ModelRenderer SymbolBRenderer;\n\t\tpublic GameObject Face;\n\t\tpublic ModelRenderer FaceRenderer;\n\t\tpublic GameObject Halo;\n\t\tpublic ModelRenderer HaloRenderer;\n\t\tpublic GameObject Guide;\n\t\tpublic ModelRenderer GuideRenderer;\n\t\tpublic GameObject SpecialBadge;\n\t\tpublic ModelRenderer SpecialBadgeRenderer;\n\t\tpublic int Type;\n\t\tpublic SpecialKind Special;\n\t\tpublic Vector3 Anchor; // resting/target world position\n\t\tpublic Vector3 Velocity; // for falling\n\t\tpublic float BobPhase;\n\t\tpublic float ClearTimer; // > 0 while playing destroy anim\n\t\tpublic bool Clearing;\n\t\tpublic bool Spawning; // newly spawned, scaling up from 0\n\t\tpublic float SpawnTimer;\n\t\tpublic float SwapT; // 0..1 progress for swap tween\n\t\tpublic Vector3 SwapFrom;\n\t\tpublic Vector3 SwapTo;\n\t\tpublic bool Swapping;\n\t}\n\n\tsealed class Burst\n\t{\n\t\tpublic GameObject Go;\n\t\tpublic ModelRenderer Renderer;\n\t\tpublic Vector3 Velocity;\n\t\tpublic float Life;\n\t\tpublic float MaxLife;\n\t\tpublic Color BaseTint;\n\t\tpublic float StartScale;\n\t}\n\n\treadonly List<Burst> _bursts = new();\n\n\t// Particle pool. Bursts/fire spawn dozens of short-lived renderer objects per\n\t// clear; recycling them instead of new/Destroy each time avoids GC churn.\n\treadonly Stack<GameObject> _burstPool = new();\n\tconst int MaxPooledBursts = 256;\n\n\tGameObject RentBurstObject( string name )\n\t{\n\t\tGameObject go;\n\t\tif ( _burstPool.Count > 0 )\n\t\t{\n\t\t\tgo = _burstPool.Pop();\n\t\t\tgo.Name = name;\n\t\t\tgo.Enabled = true;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tgo = new GameObject( true, name );\n\t\t\tgo.Flags = GameObjectFlags.NotNetworked;\n\t\t\tgo.AddComponent<ModelRenderer>();\n\t\t}\n\t\treturn go;\n\t}\n\n\tvoid ReturnBurstObject( GameObject go )\n\t{\n\t\tif ( go == null || !go.IsValid() ) return;\n\t\tif ( _burstPool.Count >= MaxPooledBursts )\n\t\t{\n\t\t\tgo.Destroy();\n\t\t\treturn;\n\t\t}\n\t\tgo.Enabled = false;\n\t\tgo.SetParent( null );\n\t\t_burstPool.Push( go );\n\t}\n\n\tsealed class Shockwave\n\t{\n\t\tpublic GameObject Go;\n\t\tpublic ModelRenderer Renderer;\n\t\tpublic Color Tint;\n\t\tpublic float Life;\n\t\tpublic float MaxLife;\n\t\tpublic float Radius;\n\t}\n\n\treadonly List<Shockwave> _shockwaves = new();\n\n\tfloat _audioCooldown;\n\tSoundHandle _music;\n\n\t// -------------------- lifecycle --------------------\n\n\tprotected override void OnStart()\n\t{\n\t\tbase.OnStart();\n\n\t\t_saveData = ColourBreakProgressStore.Load();\n\t\tApplyLevelSettings();\n\t\t_boxModel = Model.Load( \"models/dev/box.vmdl\" );\n\t\t_sphereModel = Model.Load( \"models/dev/sphere.vmdl\" );\n\t\tCandyModel ??= _boxModel;\n\n\t\tEnsureSceneLighting();\n\t\tBuildEnvironment();\n\t\tBuildFreshBoard();\n\n\t\tif ( AutoFrameMainCamera )\n\t\t\tFrameMainCameraToBoard();\n\n\t\tMouse.Visibility = MouseVisibility.Visible;\n\n\t\t// Start at the main menu; the board sits in the background as a live backdrop.\n\t\tCurrentState = GameState.Menu;\n\n\t\tLog.Info( \"Colour Break ready. Press Play to start. Drag a colour piece onto an adjacent one to swap. R resets, Esc pauses.\" );\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tHandlePauseHotkeys();\n\t\tUpdateMusic();\n\n\t\t// Paused: freeze the board entirely until the player resumes.\n\t\tif ( CurrentState == GameState.Paused )\n\t\t\treturn;\n\n\t\tAnimateCandies();\n\t\tAnimateBursts();\n\t\tAnimateShockwaves();\n\t\tUpdatePhase();\n\t\tUpdateAmbience();\n\t\tUpdateCameraPunch();\n\t\t_audioCooldown = MathF.Max( 0f, _audioCooldown - Time.Delta );\n\t\t_controllerMoveCooldown = MathF.Max( 0f, _controllerMoveCooldown - Time.Delta );\n\n\t\t// Only accept gameplay input while actually playing.\n\t\tif ( CurrentState == GameState.Playing && _phase == Phase.Idle )\n\t\t{\n\t\t\tHandleMouse();\n\t\t\tHandleInput();\n\t\t}\n\n\t\tUpdateHint();\n\t\tApplyHighlights();\n\t}\n\n\t// -------------------- game state / menu API --------------------\n\n\tvoid ApplyLevelSettings()\n\t{\n\t\tLevelIndex = Math.Clamp( LevelIndex, 1, ColourBreakLevels.Count );\n\n\t\tvar level = CurrentLevelDefinition;\n\t\tTargetScore = level.TargetScore;\n\t\tMoveLimit = level.MoveLimit;\n\t\tCandyTypeCount = Math.Clamp( level.ColourCount, 4, Math.Min( Palette.Length, Shapes.Length ) );\n\t}\n\n\tpublic int GetBestStars( int levelIndex )\n\t{\n\t\treturn ColourBreakProgressStore.GetBestStars( _saveData, levelIndex );\n\t}\n\n\tvoid SaveWinProgress()\n\t{\n\t\tColourBreakProgressStore.RecordWin( _saveData, LevelIndex, CompletionStars, HasNextLevel );\n\t\tSubmitLeaderboardStats();\n\t}\n\n\t// Cumulative stats feed s&box leaderboards once the published package defines\n\t// them. Guarded so it is a harmless no-op in the editor / offline.\n\tvoid SubmitLeaderboardStats()\n\t{\n\t\ttry\n\t\t{\n\t\t\tSandbox.Services.Stats.Increment( \"levels-cleared\", 1 );\n\t\t\tSandbox.Services.Stats.Increment( \"stars-earned\", CompletionStars );\n\t\t\tSandbox.Services.Stats.Increment( \"total-score\", _score );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Info( $\"Leaderboard stats unavailable: {e.Message}\" );\n\t\t}\n\t}\n\n\tstring ObjectiveLabel()\n\t{\n\t\tvar level = CurrentLevelDefinition;\n\t\treturn level.ObjectiveKind switch\n\t\t{\n\t\t\tColourBreakObjectiveKind.ClearPieces => $\"Break {level.ObjectiveTarget} pieces\",\n\t\t\tColourBreakObjectiveKind.ClearColour => $\"Break {level.ObjectiveTarget} {ColourName( level.ObjectiveType )}\",\n\t\t\tColourBreakObjectiveKind.MakeSpecials => $\"Create {level.ObjectiveTarget} specials\",\n\t\t\tColourBreakObjectiveKind.TriggerSpecials => $\"Trigger {level.ObjectiveTarget} specials\",\n\t\t\tColourBreakObjectiveKind.ChainCombos => $\"Chain {level.ObjectiveTarget} cascades\",\n\t\t\t_ => $\"Score {TargetScore}\"\n\t\t};\n\t}\n\n\tstring ColourName( int type )\n\t{\n\t\tif ( type >= 0 && type < ColourNames.Length )\n\t\t\treturn ColourNames[type];\n\n\t\treturn \"colour\";\n\t}\n\n\tvoid ApplyEndlessSettings()\n\t{\n\t\tCandyTypeCount = Math.Clamp( EndlessColourCount, 4, Math.Min( Palette.Length, Shapes.Length ) );\n\t\tTargetScore = int.MaxValue; // no score win condition in endless\n\t\tMoveLimit = int.MaxValue; // no move limit in endless\n\t}\n\n\t// Switch the selected mode from the main menu and refresh the backdrop board.\n\tpublic void SetMode( GameMode mode )\n\t{\n\t\tif ( CurrentState != GameState.Menu || Mode == mode )\n\t\t\treturn;\n\n\t\tMode = mode;\n\t\tif ( IsEndless ) ApplyEndlessSettings();\n\t\telse ApplyLevelSettings();\n\t\tBuildFreshBoard();\n\t}\n\n\tpublic void SelectLevelsMode() => SetMode( GameMode.Levels );\n\tpublic void SelectEndlessMode() => SetMode( GameMode.Endless );\n\n\tpublic void StartNewGame()\n\t{\n\t\tEndlessRecordSet = false;\n\n\t\tif ( IsEndless )\n\t\t{\n\t\t\tApplyEndlessSettings();\n\t\t}\n\t\telse\n\t\t{\n\t\t\tApplyLevelSettings();\n\t\t\tif ( SelectedLevelLocked )\n\t\t\t{\n\t\t\t\tLevelIndex = HighestUnlockedLevel;\n\t\t\t\tApplyLevelSettings();\n\t\t\t}\n\t\t}\n\n\t\tBuildFreshBoard();\n\t\tCurrentState = GameState.Playing;\n\t\t_comboCount = 0;\n\t\tPlayUiSound( StartSound, 0.85f, 1f );\n\t\tSpawnFireColumn( BoardOrigin + new Vector3( 0f, -14f, 0f ), 0.80f );\n\t\tSpawnShockwave( BoardOrigin, new Color( 1f, 0.42f, 0.08f ), 0.55f, 110f );\n\t\tPunchCamera( 3.5f, 2.0f );\n\t\tMouse.Visibility = MouseVisibility.Visible;\n\t}\n\n\tpublic void RestartGame() => StartNewGame();\n\n\tpublic void StartNextLevel()\n\t{\n\t\tif ( HasNextLevel && LevelIndex < HighestUnlockedLevel )\n\t\t\tLevelIndex++;\n\n\t\tStartNewGame();\n\t}\n\n\tpublic void SelectPreviousLevel()\n\t{\n\t\tif ( CurrentState != GameState.Menu )\n\t\t\treturn;\n\n\t\tLevelIndex = Math.Max( 1, LevelIndex - 1 );\n\t\tApplyLevelSettings();\n\t\tBuildFreshBoard();\n\t}\n\n\tpublic void SelectNextLevel()\n\t{\n\t\tif ( CurrentState != GameState.Menu )\n\t\t\treturn;\n\n\t\tLevelIndex = Math.Min( LevelCount, LevelIndex + 1 );\n\t\tApplyLevelSettings();\n\t\tBuildFreshBoard();\n\t}\n\n\tpublic void PauseGame()\n\t{\n\t\tif ( CurrentState == GameState.Playing )\n\t\t\tCurrentState = GameState.Paused;\n\t}\n\n\tpublic void ResumeGame()\n\t{\n\t\tif ( CurrentState == GameState.Paused )\n\t\t\tCurrentState = GameState.Playing;\n\t}\n\n\tpublic void QuitToMenu()\n\t{\n\t\tif ( IsEndless ) ApplyEndlessSettings();\n\t\telse ApplyLevelSettings();\n\t\tBuildFreshBoard(); // rebuild a fresh backdrop and reset stats\n\t\tCurrentState = GameState.Menu;\n\t}\n\n\tpublic void ToggleAudio() => AudioEnabled = !AudioEnabled;\n\tpublic void ToggleMusic() => MusicEnabled = !MusicEnabled;\n\tpublic void ToggleEffects() => EffectsEnabled = !EffectsEnabled;\n\tpublic void ToggleCameraShake() => CameraShakeEnabled = !CameraShakeEnabled;\n\tpublic void ToggleColourBlindMode()\n\t{\n\t\tColourBlindMode = !ColourBlindMode;\n\t\tRefreshAllPieceVisuals();\n\t}\n\n\t// -------------------- bug reporting --------------------\n\n\t// Reports are stored locally; only this Steam ID sees the in-game manager.\n\tconst long DevSteamId = 76561198271608270L;\n\n\tpublic bool IsDeveloper => LocalSteamId() == DevSteamId;\n\tpublic int OpenBugReportCount => ColourBreakBugReportStore.OpenCount();\n\n\tstatic long LocalSteamId()\n\t{\n\t\ttry { return Connection.Local?.SteamId.Value ?? 0L; }\n\t\tcatch { return 0L; }\n\t}\n\n\tstring LocalReporterName()\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar c = Connection.Local;\n\t\t\tif ( c != null && !string.IsNullOrWhiteSpace( c.DisplayName ) )\n\t\t\t\treturn c.DisplayName;\n\t\t}\n\t\tcatch { }\n\t\treturn \"Player\";\n\t}\n\n\tstring BugContext()\n\t{\n\t\tif ( IsEndless )\n\t\t\treturn $\"Endless - Score {_score}, Moves {_moves}\";\n\t\treturn $\"Level {LevelIndex} ({LevelName}) - {CurrentState}\";\n\t}\n\n\tpublic bool SubmitBugReport( string message )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( message ) )\n\t\t\treturn false;\n\n\t\tvar report = new ColourBreakBugReport\n\t\t{\n\t\t\tMessage = message,\n\t\t\tReporter = LocalReporterName(),\n\t\t\tSteamId = LocalSteamId(),\n\t\t\tContext = BugContext(),\n\t\t\tVersion = \"1.0\",\n\t\t};\n\n\t\tbool ok = ColourBreakBugReportStore.Add( report );\n\t\tif ( ok )\n\t\t\tPlayUiSound( ClearSound, 0.6f, 1.1f );\n\t\treturn ok;\n\t}\n\n\tpublic List<ColourBreakBugReport> GetBugReports() => ColourBreakBugReportStore.GetAll();\n\tpublic void ResolveBugReport( string id ) => ColourBreakBugReportStore.SetResolved( id, true );\n\tpublic void ReopenBugReport( string id ) => ColourBreakBugReportStore.SetResolved( id, false );\n\tpublic void DeleteBugReport( string id ) => ColourBreakBugReportStore.Delete( id );\n\tpublic void ClearBugReports() => ColourBreakBugReportStore.Clear();\n\n\tvoid HandlePauseHotkeys()\n\t{\n\t\t// Esc toggles pause while in a game; ignored on the main menu.\n\t\tif ( CurrentState == GameState.Menu || CurrentState == GameState.Won || CurrentState == GameState.Lost ) return;\n\n\t\tif ( Pressed( \"Escape\" ) || ActionPressed( \"menu\", \"pause\" ) )\n\t\t{\n\t\t\tif ( CurrentState == GameState.Playing ) PauseGame();\n\t\t\telse if ( CurrentState == GameState.Paused ) ResumeGame();\n\t\t}\n\t}\n\n\t// -------------------- environment --------------------\n\n\tvoid BuildEnvironment()\n\t{\n\t\tCleanupDecor();\n\n\t\tfloat boardW = Width * CellSpacing;\n\t\tfloat boardH = Height * CellSpacing;\n\t\tfloat edge = 18f;\n\t\tfloat depth = 28f;\n\n\t\tAddStageBackdrop( boardW, boardH, edge, depth );\n\n\t\t_decor.Add( CreateBlock( \"Tray\",\n\t\t\tBoardOrigin + new Vector3( 0f, depth * 0.5f + 8f, 0f ),\n\t\t\tnew Vector3( boardW + edge * 6.8f, depth, boardH + edge * 6.8f ),\n\t\t\tnew Color( 0.025f, 0.028f, 0.050f ) ) );\n\n\t\tvar trayInner = CreateBlock( \"TrayInner\",\n\t\t\tBoardOrigin + new Vector3( 0f, depth * 0.5f + 2f, 0f ),\n\t\t\tnew Vector3( boardW + edge * 2.6f, depth - 4f, boardH + edge * 2.6f ),\n\t\t\tnew Color( 0.055f, 0.065f, 0.105f ) );\n\t\t_trayInnerRenderer = trayInner.GetComponent<ModelRenderer>();\n\t\t_trayBaseColor = new Color( 0.055f, 0.065f, 0.105f );\n\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t{\n\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t{\n\t\t\t\tfloat cellX = (x - (Width - 1) * 0.5f) * CellSpacing;\n\t\t\t\tfloat cellZ = (y - (Height - 1) * 0.5f) * CellSpacing;\n\t\t\t\tfloat alt = ((x + y) & 1) == 0 ? 0.00f : 0.018f;\n\t\t\t\tvar tile = CreateBlock( $\"WallTile_{x}_{y}\",\n\t\t\t\t\tBoardOrigin + new Vector3( cellX, depth * 0.5f - 2f, cellZ ),\n\t\t\t\t\tnew Vector3( CellSpacing * 0.86f, 3.5f, CellSpacing * 0.86f ),\n\t\t\t\t\tnew Color( 0.075f + alt, 0.085f + alt, 0.135f + alt ) );\n\t\t\t\ttile.WorldRotation = Rotation.FromYaw( ((x + y) % 3 - 1) * 1.4f );\n\t\t\t}\n\t\t}\n\n\t\tfor ( int x = 0; x <= Width; x++ )\n\t\t{\n\t\t\tfloat gridX = (x - Width * 0.5f) * CellSpacing + CellSpacing * 0.5f;\n\t\t\t_decor.Add( CreateBlock( $\"GridV_{x}\",\n\t\t\t\tBoardOrigin + new Vector3( gridX, depth * 0.5f - 5f, 0f ),\n\t\t\t\tnew Vector3( 1.2f, 1.8f, boardH + edge * 0.6f ),\n\t\t\t\tnew Color( 0.025f, 0.22f, 0.34f ) ) );\n\t\t}\n\n\t\tfor ( int y = 0; y <= Height; y++ )\n\t\t{\n\t\t\tfloat gridZ = (y - Height * 0.5f) * CellSpacing + CellSpacing * 0.5f;\n\t\t\t_decor.Add( CreateBlock( $\"GridH_{y}\",\n\t\t\t\tBoardOrigin + new Vector3( 0f, depth * 0.5f - 5f, gridZ ),\n\t\t\t\tnew Vector3( boardW + edge * 0.6f, 1.8f, 1.2f ),\n\t\t\t\tnew Color( 0.025f, 0.22f, 0.34f ) ) );\n\t\t}\n\n\t\tvar frameTint = new Color( 0.19f, 0.10f, 0.31f );\n\t\t_decor.Add( CreateBlock( \"FrameTop\",\n\t\t\tBoardOrigin + new Vector3( 0f, -4f, boardH * 0.5f + edge * 1.0f ),\n\t\t\tnew Vector3( boardW + edge * 5.2f, edge * 1.8f, edge * 1.8f ), frameTint ) );\n\t\t_decor.Add( CreateBlock( \"FrameBottom\",\n\t\t\tBoardOrigin + new Vector3( 0f, -4f, -boardH * 0.5f - edge * 1.0f ),\n\t\t\tnew Vector3( boardW + edge * 5.2f, edge * 1.8f, edge * 1.8f ), frameTint ) );\n\t\t_decor.Add( CreateBlock( \"FrameLeft\",\n\t\t\tBoardOrigin + new Vector3( -boardW * 0.5f - edge * 1.0f, -4f, 0f ),\n\t\t\tnew Vector3( edge * 1.8f, edge * 1.8f, boardH + edge * 5.2f ), frameTint ) );\n\t\t_decor.Add( CreateBlock( \"FrameRight\",\n\t\t\tBoardOrigin + new Vector3( boardW * 0.5f + edge * 1.0f, -4f, 0f ),\n\t\t\tnew Vector3( edge * 1.8f, edge * 1.8f, boardH + edge * 5.2f ), frameTint ) );\n\n\t\tvar railTint = new Color( 0.05f, 0.78f, 1f );\n\t\t_decor.Add( CreateBlock( \"GlowRailTop\",\n\t\t\tBoardOrigin + new Vector3( 0f, -12f, boardH * 0.5f + edge * 2.2f ),\n\t\t\tnew Vector3( boardW + edge * 4.6f, edge * 0.28f, edge * 0.30f ), railTint ) );\n\t\t_decor.Add( CreateBlock( \"GlowRailBottom\",\n\t\t\tBoardOrigin + new Vector3( 0f, -12f, -boardH * 0.5f - edge * 2.2f ),\n\t\t\tnew Vector3( boardW + edge * 4.6f, edge * 0.28f, edge * 0.30f ), new Color( 1f, 0.28f, 0.05f ) ) );\n\t\t_decor.Add( CreateBlock( \"GlowRailLeft\",\n\t\t\tBoardOrigin + new Vector3( -boardW * 0.5f - edge * 2.2f, -12f, 0f ),\n\t\t\tnew Vector3( edge * 0.30f, edge * 0.28f, boardH + edge * 4.6f ), new Color( 0.82f, 0.16f, 1f ) ) );\n\t\t_decor.Add( CreateBlock( \"GlowRailRight\",\n\t\t\tBoardOrigin + new Vector3( boardW * 0.5f + edge * 2.2f, -12f, 0f ),\n\t\t\tnew Vector3( edge * 0.30f, edge * 0.28f, boardH + edge * 4.6f ), new Color( 1f, 0.65f, 0.08f ) ) );\n\n\t\tAddPointLight( \"AccentL\", BoardOrigin + new Vector3( -boardW * 0.72f, -boardH * 0.50f, 118f ), new Color( 1f, 0.35f, 0.95f ), 1000f );\n\t\tAddPointLight( \"AccentR\", BoardOrigin + new Vector3( boardW * 0.72f, -boardH * 0.50f, 118f ), new Color( 0.25f, 0.92f, 1f ), 1000f );\n\t\tAddPointLight( \"AccentTop\", BoardOrigin + new Vector3( 0f, -boardH * 0.62f, boardH * 0.82f ), new Color( 1f, 0.85f, 0.48f ), 1200f );\n\n\t\t_orbitLightL = AddPointLightReturn( \"OrbitL\", BoardOrigin, new Color( 1f, 0.28f, 0.08f ), 760f );\n\t\t_orbitLightR = AddPointLightReturn( \"OrbitR\", BoardOrigin, new Color( 0.25f, 0.95f, 1f ), 760f );\n\n\t\t_decor.Add( CreateBlock( \"Floor\",\n\t\t\tBoardOrigin + new Vector3( 0f, depth + 66f, -boardH * 0.5f - edge * 5.0f ),\n\t\t\tnew Vector3( boardW * 1.78f, 7f, edge * 7.0f ),\n\t\t\tnew Color( 0.018f, 0.020f, 0.035f ) ) );\n\n\t\tAddBottomCabinetLighting( boardW, boardH, edge );\n\t\tAddSideCabinetControls( -1, boardW, boardH, edge, new Color( 0.82f, 0.16f, 1f ), new Color( 1f, 0.55f, 0.08f ) );\n\t\tAddSideCabinetControls( 1, boardW, boardH, edge, new Color( 1f, 0.55f, 0.08f ), new Color( 0.05f, 0.78f, 1f ) );\n\t}\n\n\tGameObject CreateBlock( string name, Vector3 pos, Vector3 scale, Color tint )\n\t{\n\t\tvar go = new GameObject( true, name );\n\t\tgo.Flags = GameObjectFlags.NotNetworked;\n\t\tgo.WorldPosition = pos;\n\t\tgo.WorldScale = scale / 50f; // box.vmdl is ~50 units\n\t\tvar mr = go.AddComponent<ModelRenderer>();\n\t\tmr.Model = _boxModel ?? CandyModel;\n\t\tmr.Tint = tint;\n\t\t_decor.Add( go );\n\t\treturn go;\n\t}\n\n\tvoid AddStageBackdrop( float boardW, float boardH, float edge, float depth )\n\t{\n\t\tfloat backY = depth + 142f;\n\t\tvar backTint = new Color( 0.030f, 0.028f, 0.056f );\n\n\t\tCreateBlock( \"StageBackdrop\",\n\t\t\tBoardOrigin + new Vector3( 0f, backY, 0f ),\n\t\t\tnew Vector3( boardW * 3.25f, 10f, boardH * 2.45f ),\n\t\t\tbackTint );\n\n\t\tCreateBlock( \"StageBackdropCore\",\n\t\t\tBoardOrigin + new Vector3( 0f, backY - 6f, 0f ),\n\t\t\tnew Vector3( boardW * 1.55f, 6f, boardH * 1.55f ),\n\t\t\tnew Color( 0.050f, 0.055f, 0.090f ) );\n\n\t\tCreateBlock( \"StageLeftWing\",\n\t\t\tBoardOrigin + new Vector3( -boardW * 1.08f, backY - 3f, 0f ),\n\t\t\tnew Vector3( edge * 3.2f, 8f, boardH * 2.28f ),\n\t\t\tnew Color( 0.052f, 0.032f, 0.085f ) );\n\n\t\tCreateBlock( \"StageRightWing\",\n\t\t\tBoardOrigin + new Vector3( boardW * 1.08f, backY - 3f, 0f ),\n\t\t\tnew Vector3( edge * 3.2f, 8f, boardH * 2.28f ),\n\t\t\tnew Color( 0.030f, 0.060f, 0.088f ) );\n\n\t\tCreateBlock( \"StageTopLine\",\n\t\t\tBoardOrigin + new Vector3( 0f, backY - 8f, boardH * 0.78f ),\n\t\t\tnew Vector3( boardW * 2.25f, 4f, edge * 0.18f ),\n\t\t\tnew Color( 0.05f, 0.78f, 1f ) );\n\n\t\tCreateBlock( \"StageBottomLine\",\n\t\t\tBoardOrigin + new Vector3( 0f, backY - 8f, -boardH * 0.78f ),\n\t\t\tnew Vector3( boardW * 2.05f, 4f, edge * 0.18f ),\n\t\t\tnew Color( 1f, 0.34f, 0.06f ) );\n\n\t\tAddPointLight( \"StageWashLeft\",\n\t\t\tBoardOrigin + new Vector3( -boardW * 0.95f, depth - 36f, boardH * 0.12f ),\n\t\t\tnew Color( 0.75f, 0.18f, 1f ),\n\t\t\t900f );\n\n\t\tAddPointLight( \"StageWashRight\",\n\t\t\tBoardOrigin + new Vector3( boardW * 0.95f, depth - 36f, -boardH * 0.08f ),\n\t\t\tnew Color( 0.08f, 0.78f, 1f ),\n\t\t\t900f );\n\t}\n\n\tvoid AddSideCabinetControls( int side, float boardW, float boardH, float edge, Color ledTint, Color buttonTint )\n\t{\n\t\tstring label = side < 0 ? \"Left\" : \"Right\";\n\t\tfloat x = side * (boardW * 0.5f + edge * 1.28f);\n\t\tfloat y = -18.5f;\n\n\t\t_decor.Add( CreateBlock( $\"Cabinet{label}Inset\",\n\t\t\tBoardOrigin + new Vector3( x, y + 0.4f, 0f ),\n\t\t\tnew Vector3( edge * 0.58f, edge * 0.52f, boardH * 0.74f ),\n\t\t\tnew Color( 0.075f, 0.045f, 0.115f ) ) );\n\n\t\t_decor.Add( CreateBlock( $\"Cabinet{label}LedRail\",\n\t\t\tBoardOrigin + new Vector3( x - side * edge * 0.18f, y - 4.2f, 0f ),\n\t\t\tnew Vector3( edge * 0.12f, edge * 0.12f, boardH * 0.70f ),\n\t\t\tledTint ) );\n\n\t\t_decor.Add( CreateBlock( $\"Cabinet{label}GoldTrimTop\",\n\t\t\tBoardOrigin + new Vector3( x - side * edge * 0.02f, y - 3.4f, boardH * 0.35f ),\n\t\t\tnew Vector3( edge * 0.42f, edge * 0.18f, edge * 0.12f ),\n\t\t\tnew Color( 1f, 0.72f, 0.22f ) ) );\n\t\t_decor.Add( CreateBlock( $\"Cabinet{label}GoldTrimBottom\",\n\t\t\tBoardOrigin + new Vector3( x - side * edge * 0.02f, y - 3.4f, -boardH * 0.35f ),\n\t\t\tnew Vector3( edge * 0.42f, edge * 0.18f, edge * 0.12f ),\n\t\t\tnew Color( 1f, 0.72f, 0.22f ) ) );\n\n\t\tAddSideButton( $\"Cabinet{label}ButtonTop\", BoardOrigin + new Vector3( x - side * edge * 0.03f, y - 5.0f, boardH * 0.22f ), buttonTint, 1.0f );\n\t\tAddSideButton( $\"Cabinet{label}ButtonBottom\", BoardOrigin + new Vector3( x - side * edge * 0.03f, y - 5.0f, -boardH * 0.18f ), Color.Lerp( buttonTint, Color.White, 0.24f ), 0.82f );\n\n\t\tAddPointLight( $\"Cabinet{label}SideGlow\",\n\t\t\tBoardOrigin + new Vector3( x - side * edge * 0.25f, y - 35f, 0f ),\n\t\t\tledTint,\n\t\t\t430f );\n\t}\n\n\tvoid AddBottomCabinetLighting( float boardW, float boardH, float edge )\n\t{\n\t\tfloat z = -boardH * 0.5f - edge * 1.12f;\n\t\tfloat y = -18.5f;\n\n\t\t_decor.Add( CreateBlock( \"CabinetBottomInset\",\n\t\t\tBoardOrigin + new Vector3( 0f, y + 0.3f, z ),\n\t\t\tnew Vector3( boardW + edge * 2.8f, edge * 0.48f, edge * 0.58f ),\n\t\t\tnew Color( 0.070f, 0.042f, 0.105f ) ) );\n\n\t\t_decor.Add( CreateBlock( \"CabinetBottomGoldRail\",\n\t\t\tBoardOrigin + new Vector3( 0f, y - 3.6f, z - edge * 0.24f ),\n\t\t\tnew Vector3( boardW + edge * 2.4f, edge * 0.14f, edge * 0.14f ),\n\t\t\tnew Color( 1f, 0.68f, 0.18f ) ) );\n\n\t\t_decor.Add( CreateBlock( \"CabinetBottomGlowRail\",\n\t\t\tBoardOrigin + new Vector3( 0f, y - 4.8f, z + edge * 0.18f ),\n\t\t\tnew Vector3( boardW + edge * 1.7f, edge * 0.10f, edge * 0.12f ),\n\t\t\tnew Color( 1f, 0.33f, 0.05f ) ) );\n\n\t\tfor ( int i = 0; i < 5; i++ )\n\t\t{\n\t\t\tfloat x = MathX.Lerp( -boardW * 0.42f, boardW * 0.42f, i / 4f );\n\t\t\tvar tint = i % 2 == 0 ? new Color( 1f, 0.48f, 0.08f ) : new Color( 0.05f, 0.78f, 1f );\n\t\t\tAddFooterLamp( $\"CabinetBottomLamp_{i}\", BoardOrigin + new Vector3( x, y - 5.4f, z + edge * 0.02f ), tint );\n\t\t}\n\n\t\tAddPointLight( \"CabinetBottomGlow\",\n\t\t\tBoardOrigin + new Vector3( 0f, y - 38f, z + edge * 0.10f ),\n\t\t\tnew Color( 1f, 0.42f, 0.08f ),\n\t\t\t520f );\n\t}\n\n\tvoid AddFooterLamp( string name, Vector3 pos, Color tint )\n\t{\n\t\tvar rim = new GameObject( true, $\"{name}Rim\" );\n\t\trim.Flags = GameObjectFlags.NotNetworked;\n\t\trim.WorldPosition = pos;\n\t\trim.WorldScale = new Vector3( 0.105f, 0.024f, 0.105f );\n\t\tvar rimRenderer = rim.AddComponent<ModelRenderer>();\n\t\trimRenderer.Model = _sphereModel ?? _boxModel ?? CandyModel;\n\t\trimRenderer.Tint = new Color( 0.40f, 0.42f, 0.50f );\n\t\t_decor.Add( rim );\n\n\t\tvar face = new GameObject( true, $\"{name}Face\" );\n\t\tface.Flags = GameObjectFlags.NotNetworked;\n\t\tface.WorldPosition = pos + new Vector3( 0f, -1.4f, 0f );\n\t\tface.WorldScale = new Vector3( 0.070f, 0.018f, 0.070f );\n\t\tvar faceRenderer = face.AddComponent<ModelRenderer>();\n\t\tfaceRenderer.Model = _sphereModel ?? _boxModel ?? CandyModel;\n\t\tfaceRenderer.Tint = tint;\n\t\t_decor.Add( face );\n\t}\n\n\tvoid AddSideButton( string name, Vector3 pos, Color tint, float size )\n\t{\n\t\tvar rim = new GameObject( true, $\"{name}Rim\" );\n\t\trim.Flags = GameObjectFlags.NotNetworked;\n\t\trim.WorldPosition = pos;\n\t\trim.WorldScale = new Vector3( 0.26f * size, 0.070f * size, 0.26f * size );\n\t\tvar rimRenderer = rim.AddComponent<ModelRenderer>();\n\t\trimRenderer.Model = _sphereModel ?? _boxModel ?? CandyModel;\n\t\trimRenderer.Tint = new Color( 0.46f, 0.48f, 0.58f );\n\t\t_decor.Add( rim );\n\n\t\tvar face = new GameObject( true, $\"{name}Face\" );\n\t\tface.Flags = GameObjectFlags.NotNetworked;\n\t\tface.WorldPosition = pos + new Vector3( 0f, -2.4f, 0f );\n\t\tface.WorldScale = new Vector3( 0.18f * size, 0.050f * size, 0.18f * size );\n\t\tvar faceRenderer = face.AddComponent<ModelRenderer>();\n\t\tfaceRenderer.Model = _sphereModel ?? _boxModel ?? CandyModel;\n\t\tfaceRenderer.Tint = tint;\n\t\t_decor.Add( face );\n\n\t\tvar shine = new GameObject( true, $\"{name}Shine\" );\n\t\tshine.Flags = GameObjectFlags.NotNetworked;\n\t\tshine.WorldPosition = pos + new Vector3( -3.4f * size, -4.2f, 4.2f * size );\n\t\tshine.WorldScale = new Vector3( 0.055f * size, 0.018f * size, 0.032f * size );\n\t\tvar shineRenderer = shine.AddComponent<ModelRenderer>();\n\t\tshineRenderer.Model = _sphereModel ?? _boxModel ?? CandyModel;\n\t\tshineRenderer.Tint = new Color( 1f, 1f, 1f, 0.82f );\n\t\t_decor.Add( shine );\n\t}\n\n\tvoid AddPointLight( string name, Vector3 pos, Color color, float radius )\n\t{\n\t\tvar go = new GameObject( true, name );\n\t\tgo.Flags = GameObjectFlags.NotNetworked;\n\t\tgo.WorldPosition = pos;\n\t\tvar l = go.AddComponent<PointLight>();\n\t\tl.LightColor = color;\n\t\tl.Radius = radius;\n\t\t_decor.Add( go );\n\t}\n\n\tGameObject AddPointLightReturn( string name, Vector3 pos, Color color, float radius )\n\t{\n\t\tvar go = new GameObject( true, name );\n\t\tgo.Flags = GameObjectFlags.NotNetworked;\n\t\tgo.WorldPosition = pos;\n\t\tvar l = go.AddComponent<PointLight>();\n\t\tl.LightColor = color;\n\t\tl.Radius = radius;\n\t\t_decor.Add( go );\n\t\treturn go;\n\t}\n\n\t// -------------------- board --------------------\n\n\tvoid BuildFreshBoard()\n\t{\n\t\tClearBoardObjects();\n\n\t\t_types = new int[Width, Height];\n\t\t_specials = new SpecialKind[Width, Height];\n\t\t_visuals = new CandyVisual[Width, Height];\n\n\t\tint attempts = 0;\n\t\tdo\n\t\t{\n\t\t\tClearBoardObjects();\n\t\t\t_types = new int[Width, Height];\n\t\t\t_specials = new SpecialKind[Width, Height];\n\t\t\t_visuals = new CandyVisual[Width, Height];\n\t\t\tfor ( int y = 0; y < Height; y++ )\n\t\t\t{\n\t\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t\t{\n\t\t\t\t\tint type = PickTypeAvoidingImmediateMatch( x, y );\n\t\t\t\t\tSpawnCandy( x, y, type, instant: true );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\twhile ( !HasPossibleMove() && ++attempts < 20 );\n\n\t\t_cursorX = Width / 2;\n\t\t_cursorY = Height / 2;\n\t\t_hasSelection = false;\n\t\t_score = 0;\n\t\t_moves = 0;\n\t\t_objectiveProgress = 0;\n\t\t_phase = Phase.Idle;\n\t\t_idleTime = 0f;\n\t\t_hasHint = false;\n\t}\n\n\tvoid SpawnCandy( int x, int y, int type, bool instant )\n\t{\n\t\t_types[x, y] = type;\n\n\t\tvar anchor = CellToWorld( x, y );\n\t\tvar go = new GameObject( true, $\"Piece_{x}_{y}\" );\n\t\tgo.Flags = GameObjectFlags.NotNetworked;\n\t\tgo.WorldRotation = Rotation.FromYaw( Game.Random.Float( -7f, 7f ) );\n\t\tvar renderer = go.AddComponent<ModelRenderer>();\n\t\trenderer.Model = ModelForType( type );\n\t\tvar pieceColor = PieceColor( type );\n\t\trenderer.Tint = Color.Lerp( pieceColor, Color.Black, 0.28f );\n\n\t\tvar rim = AddPiecePart( go, $\"PieceRim_{x}_{y}\", _sphereModel ?? _boxModel,\n\t\t\tnew Vector3( 0f, -4.0f, 0f ), new Vector3( 1.08f, 0.24f, 1.08f ), Color.Lerp( pieceColor, Color.Black, 0.42f ) );\n\t\tvar face = AddPiecePart( go, $\"PieceFace_{x}_{y}\", _sphereModel ?? _boxModel,\n\t\t\tnew Vector3( 0f, -9.5f, 0.6f ), new Vector3( 0.76f, 0.14f, 0.76f ), Color.Lerp( pieceColor, Color.White, 0.20f ) );\n\t\tvar halo = AddPiecePart( go, $\"PieceHalo_{x}_{y}\", _sphereModel ?? _boxModel,\n\t\t\tnew Vector3( 0f, -5.4f, 0f ), Vector3.Zero, new Color( 1f, 0.95f, 0.22f, 0.82f ) );\n\t\tvar guide = AddPiecePart( go, $\"PieceGuide_{x}_{y}\", _sphereModel ?? _boxModel,\n\t\t\tnew Vector3( 0f, -5.0f, 0f ), Vector3.Zero, new Color( 0.22f, 0.92f, 1f, 0.62f ) );\n\t\tvar accent = AddPiecePart( go, $\"PieceAccent_{x}_{y}\", _boxModel ?? _sphereModel,\n\t\t\tnew Vector3( 0f, -13.0f, 7.0f ), new Vector3( 0.34f, 0.034f, 0.048f + (type % 3) * 0.020f ), Color.Lerp( pieceColor, Color.White, 0.70f ) );\n\t\taccent.LocalRotation = Rotation.FromYaw( type * 28f );\n\t\tvar symbolA = AddPiecePart( go, $\"PieceSymbolA_{x}_{y}\", _boxModel ?? _sphereModel,\n\t\t\tnew Vector3( 0f, -18.0f, 0.0f ), Vector3.Zero, new Color( 0.02f, 0.025f, 0.035f, 1f ) );\n\t\tvar symbolB = AddPiecePart( go, $\"PieceSymbolB_{x}_{y}\", _boxModel ?? _sphereModel,\n\t\t\tnew Vector3( 0f, -18.4f, 0.0f ), Vector3.Zero, new Color( 1f, 1f, 1f, 1f ) );\n\t\tvar shine = AddPiecePart( go, $\"PieceShine_{x}_{y}\", _sphereModel ?? _boxModel,\n\t\t\tnew Vector3( -8.5f, -14.0f, 9.0f ), new Vector3( 0.16f, 0.026f, 0.080f ), new Color( 1f, 1f, 1f, 0.86f ) );\n\t\tvar badge = AddPiecePart( go, $\"PieceSpecial_{x}_{y}\", _boxModel ?? _sphereModel,\n\t\t\tnew Vector3( 0f, -15.0f, -8.0f ), Vector3.Zero, new Color( 1f, 0.82f, 0.18f, 1f ) );\n\n\t\tvar v = new CandyVisual\n\t\t{\n\t\t\tGo = go,\n\t\t\tRenderer = renderer,\n\t\t\tShine = shine,\n\t\t\tShineRenderer = shine.GetComponent<ModelRenderer>(),\n\t\t\tRim = rim,\n\t\t\tRimRenderer = rim.GetComponent<ModelRenderer>(),\n\t\t\tFace = face,\n\t\t\tFaceRenderer = face.GetComponent<ModelRenderer>(),\n\t\t\tHalo = halo,\n\t\t\tHaloRenderer = halo.GetComponent<ModelRenderer>(),\n\t\t\tGuide = guide,\n\t\t\tGuideRenderer = guide.GetComponent<ModelRenderer>(),\n\t\t\tAccent = accent,\n\t\t\tAccentRenderer = accent.GetComponent<ModelRenderer>(),\n\t\t\tSymbolA = symbolA,\n\t\t\tSymbolARenderer = symbolA.GetComponent<ModelRenderer>(),\n\t\t\tSymbolB = symbolB,\n\t\t\tSymbolBRenderer = symbolB.GetComponent<ModelRenderer>(),\n\t\t\tSpecialBadge = badge,\n\t\t\tSpecialBadgeRenderer = badge.GetComponent<ModelRenderer>(),\n\t\t\tType = type,\n\t\t\tSpecial = SpecialKind.None,\n\t\t\tAnchor = anchor,\n\t\t\tBobPhase = Game.Random.Float( 0f, MathF.PI * 2f ),\n\t\t\tSpawning = !instant,\n\t\t\tSpawnTimer = instant ? 0f : 0.25f,\n\t\t};\n\n\t\tif ( instant )\n\t\t{\n\t\t\tgo.WorldPosition = anchor;\n\t\t\tgo.WorldScale = Vector3.One * CandyScale * PieceShape( type );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tgo.WorldPosition = anchor + new Vector3( 0f, 0f, Height * CellSpacing * 0.6f + Game.Random.Float( 0f, 60f ) );\n\t\t\tgo.WorldScale = Vector3.One * CandyScale * PieceShape( type ) * 0.4f;\n\t\t}\n\n\t\t_visuals[x, y] = v;\n\t\tApplyPieceVisualStyle( v );\n\t}\n\n\tGameObject AddPiecePart( GameObject parent, string name, Model model, Vector3 localPosition, Vector3 localScale, Color tint )\n\t{\n\t\tvar part = new GameObject( true, name );\n\t\tpart.Flags = GameObjectFlags.NotNetworked;\n\t\tpart.SetParent( parent, false );\n\t\tpart.LocalPosition = localPosition;\n\t\tpart.LocalScale = localScale;\n\t\tvar mr = part.AddComponent<ModelRenderer>();\n\t\tmr.Model = model;\n\t\tmr.Tint = tint;\n\t\treturn part;\n\t}\n\n\tvoid ApplyPieceVisualStyle( CandyVisual v )\n\t{\n\t\tif ( v == null ) return;\n\n\t\tvar color = PieceColor( v.Type );\n\t\tif ( v.Renderer != null ) v.Renderer.Tint = Color.Lerp( color, Color.Black, 0.28f );\n\t\tif ( v.RimRenderer != null ) v.RimRenderer.Tint = Color.Lerp( color, Color.Black, 0.42f );\n\t\tif ( v.FaceRenderer != null ) v.FaceRenderer.Tint = Color.Lerp( color, Color.White, 0.20f );\n\t\tif ( v.AccentRenderer != null ) v.AccentRenderer.Tint = Color.Lerp( color, Color.White, 0.70f );\n\n\t\tApplyColourBlindSymbol( v );\n\t}\n\n\tvoid ApplyColourBlindSymbol( CandyVisual v )\n\t{\n\t\tif ( v?.SymbolA == null || v.SymbolB == null )\n\t\t\treturn;\n\n\t\tif ( !ColourBlindMode )\n\t\t{\n\t\t\tv.SymbolA.LocalScale = Vector3.Zero;\n\t\t\tv.SymbolB.LocalScale = Vector3.Zero;\n\t\t\treturn;\n\t\t}\n\n\t\tv.SymbolA.LocalRotation = Rotation.Identity;\n\t\tv.SymbolB.LocalRotation = Rotation.Identity;\n\t\tv.SymbolA.LocalScale = Vector3.Zero;\n\t\tv.SymbolB.LocalScale = Vector3.Zero;\n\t\tif ( v.SymbolARenderer != null ) v.SymbolARenderer.Tint = new Color( 0.02f, 0.025f, 0.035f, 1f );\n\t\tif ( v.SymbolBRenderer != null ) v.SymbolBRenderer.Tint = new Color( 1f, 1f, 1f, 1f );\n\n\t\tswitch ( v.Type )\n\t\t{\n\t\t\tcase 0:\n\t\t\t\tv.SymbolB.LocalScale = new Vector3( 0.19f, 0.036f, 0.70f );\n\t\t\t\tv.SymbolA.LocalScale = new Vector3( 0.11f, 0.040f, 0.58f );\n\t\t\t\tbreak;\n\t\t\tcase 1:\n\t\t\t\tv.SymbolB.LocalScale = new Vector3( 0.70f, 0.036f, 0.19f );\n\t\t\t\tv.SymbolA.LocalScale = new Vector3( 0.58f, 0.040f, 0.11f );\n\t\t\t\tbreak;\n\t\t\tcase 2:\n\t\t\t\tv.SymbolB.LocalScale = new Vector3( 0.68f, 0.036f, 0.17f );\n\t\t\t\tv.SymbolB.LocalRotation = Rotation.FromYaw( 45f );\n\t\t\t\tv.SymbolA.LocalScale = new Vector3( 0.56f, 0.040f, 0.10f );\n\t\t\t\tv.SymbolA.LocalRotation = Rotation.FromYaw( 45f );\n\t\t\t\tbreak;\n\t\t\tcase 3:\n\t\t\t\tv.SymbolA.LocalScale = new Vector3( 0.60f, 0.040f, 0.12f );\n\t\t\t\tv.SymbolB.LocalScale = new Vector3( 0.12f, 0.042f, 0.60f );\n\t\t\t\tbreak;\n\t\t\tcase 4:\n\t\t\t\tv.SymbolB.LocalScale = new Vector3( 0.42f, 0.036f, 0.42f );\n\t\t\t\tv.SymbolA.LocalScale = new Vector3( 0.30f, 0.040f, 0.30f );\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tv.SymbolA.LocalScale = new Vector3( 0.60f, 0.040f, 0.12f );\n\t\t\t\tv.SymbolA.LocalRotation = Rotation.FromYaw( -45f );\n\t\t\t\tv.SymbolB.LocalScale = new Vector3( 0.60f, 0.042f, 0.12f );\n\t\t\t\tv.SymbolB.LocalRotation = Rotation.FromYaw( 45f );\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tvoid RefreshAllPieceVisuals()\n\t{\n\t\tif ( _visuals == null ) return;\n\n\t\tfor ( int y = 0; y < _visuals.GetLength( 1 ); y++ )\n\t\t\tfor ( int x = 0; x < _visuals.GetLength( 0 ); x++ )\n\t\t\t\tApplyPieceVisualStyle( _visuals[x, y] );\n\t}\n\n\tvoid RemoveCandyAt( int x, int y )\n\t{\n\t\tvar v = _visuals[x, y];\n\t\tv?.Go?.Destroy();\n\t\t_visuals[x, y] = null;\n\t}\n\n\tVector3 CellToWorld( int x, int y )\n\t{\n\t\tfloat ox = (x - (Width - 1) * 0.5f) * CellSpacing;\n\t\tfloat oz = (y - (Height - 1) * 0.5f) * CellSpacing;\n\t\treturn BoardOrigin + new Vector3( ox, 0f, oz );\n\t}\n\n\t// -------------------- input --------------------\n\n\tbool TryHitBoardCell( out int cx, out int cy )\n\t{\n\t\tcx = cy = -1;\n\t\tif ( !TryGetMainCamera( out var cam ) ) return false;\n\n\t\tvar ray = cam.ScreenPixelToRay( Mouse.Position );\n\t\tfloat planeY = BoardOrigin.y;\n\t\tfloat denom = ray.Forward.y;\n\t\tif ( MathF.Abs( denom ) < 0.0001f ) return false;\n\t\tfloat t = (planeY - ray.Position.y) / denom;\n\t\tif ( t <= 0 ) return false;\n\n\t\tvar hit = ray.Position + ray.Forward * t;\n\t\tfloat localX = hit.x - BoardOrigin.x + (Width - 1) * 0.5f * CellSpacing;\n\t\tfloat localZ = hit.z - BoardOrigin.z + (Height - 1) * 0.5f * CellSpacing;\n\t\tint tx = (int)MathF.Floor( localX / CellSpacing + 0.5f );\n\t\tint ty = (int)MathF.Floor( localZ / CellSpacing + 0.5f );\n\t\tif ( tx < 0 || tx >= Width || ty < 0 || ty >= Height ) return false;\n\t\tcx = tx; cy = ty;\n\t\treturn true;\n\t}\n\n\tvoid HandleMouse()\n\t{\n\t\tbool overBoard = TryHitBoardCell( out int cx, out int cy );\n\t\tif ( overBoard )\n\t\t{\n\t\t\tif ( cx != _cursorX || cy != _cursorY )\n\t\t\t\tResetIdle();\n\t\t\t_cursorX = cx;\n\t\t\t_cursorY = cy;\n\t\t}\n\n\t\tbool mouseLeft = Input.Down( \"attack1\" );\n\t\tbool justPressed = mouseLeft && !_prevMouseLeft;\n\t\tbool justReleased = !mouseLeft && _prevMouseLeft;\n\t\t_prevMouseLeft = mouseLeft;\n\n\t\tif ( overBoard && (cx != _cursorX || cy != _cursorY) )\n\t\t\tResetIdle();\n\n\t\tif ( justPressed && overBoard )\n\t\t{\n\t\t\tResetIdle();\n\t\t\t_dragging = true;\n\t\t\t_dragSwapped = false;\n\t\t\t_dragStartX = cx;\n\t\t\t_dragStartY = cy;\n\t\t\t_selectedX = cx;\n\t\t\t_selectedY = cy;\n\t\t\t_hasSelection = true;\n\t\t\tSpawnShockwave( CellToWorld( cx, cy ), new Color( 1f, 1f, 1f ), 0.28f, 26f );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( _dragging && mouseLeft && overBoard && !_dragSwapped )\n\t\t{\n\t\t\tif ( (cx != _dragStartX || cy != _dragStartY) && AreAdjacent( _dragStartX, _dragStartY, cx, cy ) )\n\t\t\t{\n\t\t\t\tBeginSwap( _dragStartX, _dragStartY, cx, cy );\n\t\t\t\t_dragSwapped = true;\n\t\t\t\t_hasSelection = false;\n\t\t\t}\n\t\t}\n\n\t\tif ( justReleased )\n\t\t{\n\t\t\t_dragging = false;\n\t\t}\n\t}\n\n\tvoid HandleInput()\n\t{\n\t\tif ( Pressed( \"r\" ) || ActionPressed( \"reload\" ) )\n\t\t{\n\t\t\tBuildFreshBoard();\n\t\t\tLog.Info( \"Board reset.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( PressedEither( \"ArrowUp\", \"w\" ) ) _cursorY = Math.Min( Height - 1, _cursorY + 1 );\n\t\tif ( PressedEither( \"ArrowDown\", \"s\" ) ) _cursorY = Math.Max( 0, _cursorY - 1 );\n\t\tif ( PressedEither( \"ArrowLeft\", \"a\" ) ) _cursorX = Math.Max( 0, _cursorX - 1 );\n\t\tif ( PressedEither( \"ArrowRight\", \"d\" ) ) _cursorX = Math.Min( Width - 1, _cursorX + 1 );\n\t\tHandleControllerCursor();\n\n\t\tif ( Pressed( \"Space\" ) || Pressed( \"Enter\" ) || Pressed( \"e\" ) || ActionPressed( \"jump\", \"use\", \"attack1\" ) )\n\t\t\tHandleSelectKey();\n\n\t\tif ( _hasSelection && ActionPressed( \"attack2\", \"duck\" ) )\n\t\t\t_hasSelection = false;\n\t}\n\n\tvoid HandleControllerCursor()\n\t{\n\t\tif ( !Input.UsingController && Input.ControllerCount <= 0 )\n\t\t\treturn;\n\n\t\tif ( _controllerMoveCooldown > 0f )\n\t\t\treturn;\n\n\t\tvar move = Input.AnalogMove;\n\t\tfloat ax = move.x;\n\t\tfloat ay = move.y;\n\t\tconst float threshold = 0.45f;\n\t\tbool moved = false;\n\n\t\tif ( MathF.Abs( ax ) > MathF.Abs( ay ) && MathF.Abs( ax ) > threshold )\n\t\t{\n\t\t\t_cursorX = Math.Clamp( _cursorX + (ax > 0f ? 1 : -1), 0, Width - 1 );\n\t\t\tmoved = true;\n\t\t}\n\t\telse if ( MathF.Abs( ay ) > threshold )\n\t\t{\n\t\t\t_cursorY = Math.Clamp( _cursorY + (ay > 0f ? 1 : -1), 0, Height - 1 );\n\t\t\tmoved = true;\n\t\t}\n\n\t\tif ( moved )\n\t\t\t_controllerMoveCooldown = 0.16f;\n\t}\n\n\tvoid HandleSelectKey()\n\t{\n\t\tif ( !_hasSelection )\n\t\t{\n\t\t\t_selectedX = _cursorX;\n\t\t\t_selectedY = _cursorY;\n\t\t\t_hasSelection = true;\n\t\t\tSpawnShockwave( CellToWorld( _cursorX, _cursorY ), new Color( 1f, 1f, 1f ), 0.28f, 26f );\n\t\t\treturn;\n\t\t}\n\t\tif ( _selectedX == _cursorX && _selectedY == _cursorY )\n\t\t{\n\t\t\t_hasSelection = false;\n\t\t\treturn;\n\t\t}\n\t\tif ( !AreAdjacent( _selectedX, _selectedY, _cursorX, _cursorY ) )\n\t\t{\n\t\t\tSpawnShockwave( CellToWorld( _cursorX, _cursorY ), new Color( 0.22f, 0.92f, 1f ), 0.22f, 18f );\n\t\t\t_selectedX = _cursorX;\n\t\t\t_selectedY = _cursorY;\n\t\t\treturn;\n\t\t}\n\t\tBeginSwap( _selectedX, _selectedY, _cursorX, _cursorY );\n\t\t_hasSelection = false;\n\t}\n\n\t// -------------------- hint system --------------------\n\n\tvoid ResetIdle()\n\t{\n\t\t_idleTime = 0f;\n\t\t_hasHint = false;\n\t}\n\n\tvoid UpdateHint()\n\t{\n\t\t// Hints only make sense while the player can actually act on the board.\n\t\tif ( CurrentState != GameState.Playing || _phase != Phase.Idle || _hasSelection )\n\t\t{\n\t\t\t_idleTime = 0f;\n\t\t\t_hasHint = false;\n\t\t\treturn;\n\t\t}\n\n\t\t_idleTime += Time.Delta;\n\t\tif ( _idleTime < HintDelay )\n\t\t{\n\t\t\t_hasHint = false;\n\t\t\treturn;\n\t\t}\n\n\t\tif ( !_hasHint )\n\t\t\t_hasHint = FindHintMove( out _hintAX, out _hintAY, out _hintBX, out _hintBY );\n\t}\n\n\tbool FindHintMove( out int ax, out int ay, out int bx, out int by )\n\t{\n\t\tax = ay = bx = by = -1;\n\t\tif ( _types == null ) return false;\n\n\t\t// Prefer activating an existing special if one is on the board.\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t{\n\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t{\n\t\t\t\tif ( _specials != null && _specials[x, y] != SpecialKind.None )\n\t\t\t\t{\n\t\t\t\t\tif ( x + 1 < Width ) { ax = x; ay = y; bx = x + 1; by = y; return true; }\n\t\t\t\t\tif ( y + 1 < Height ) { ax = x; ay = y; bx = x; by = y + 1; return true; }\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t{\n\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t{\n\t\t\t\tif ( x + 1 < Width && SwapWouldMatch( x, y, x + 1, y ) ) { ax = x; ay = y; bx = x + 1; by = y; return true; }\n\t\t\t\tif ( y + 1 < Height && SwapWouldMatch( x, y, x, y + 1 ) ) { ax = x; ay = y; bx = x; by = y + 1; return true; }\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t// -------------------- phase machine --------------------\n\n\tvoid BeginSwap( int x1, int y1, int x2, int y2 )\n\t{\n\t\t_hasEverSwapped = true;\n\t\tResetIdle();\n\t\tPlayUiSound( SwapSound, 0.55f, 0.95f + Game.Random.Float( 0f, 0.12f ) );\n\t\t_swapAX = x1; _swapAY = y1; _swapBX = x2; _swapBY = y2;\n\t\tStartSwapTween( x1, y1, x2, y2 );\n\t\t_phase = Phase.SwapForward;\n\t\t_phaseTimer = 0f;\n\t}\n\n\tvoid StartSwapTween( int x1, int y1, int x2, int y2 )\n\t{\n\t\tvar a = _visuals[x1, y1];\n\t\tvar b = _visuals[x2, y2];\n\t\tif ( a == null || b == null ) return;\n\n\t\tvar posA = CellToWorld( x1, y1 );\n\t\tvar posB = CellToWorld( x2, y2 );\n\t\ta.SwapFrom = a.Go.WorldPosition; a.SwapTo = posB; a.SwapT = 0f; a.Swapping = true; a.Anchor = posB;\n\t\tb.SwapFrom = b.Go.WorldPosition; b.SwapTo = posA; b.SwapT = 0f; b.Swapping = true; b.Anchor = posA;\n\n\t\t// Update the logical board now so subsequent logic is correct\n\t\t(_types[x1, y1], _types[x2, y2]) = (_types[x2, y2], _types[x1, y1]);\n\t\t(_specials[x1, y1], _specials[x2, y2]) = (_specials[x2, y2], _specials[x1, y1]);\n\t\t(_visuals[x1, y1], _visuals[x2, y2]) = (_visuals[x2, y2], _visuals[x1, y1]);\n\t}\n\n\tvoid UpdatePhase()\n\t{\n\t\t_phaseTimer += Time.Delta;\n\n\t\tswitch ( _phase )\n\t\t{\n\t\t\tcase Phase.Idle: return;\n\n\t\t\tcase Phase.SwapForward:\n\t\t\t\tif ( _phaseTimer >= SwapDuration )\n\t\t\t\t{\n\t\t\t\t\tvar matches = FindMatches();\n\t\t\t\t\tvar specialActivation = GetSpecialActivationForSwap();\n\t\t\t\t\tforeach ( var idx in specialActivation )\n\t\t\t\t\t\tmatches.Add( idx );\n\n\t\t\t\t\tif ( matches.Count == 0 )\n\t\t\t\t\t{\n\t\t\t\t\t\tPlayUiSound( DeniedSound, 0.55f, 0.82f );\n\t\t\t\t\t\tSpawnShockwave( (CellToWorld( _swapAX, _swapAY ) + CellToWorld( _swapBX, _swapBY )) * 0.5f, new Color( 1f, 0.18f, 0.06f ), 0.34f, 52f );\n\t\t\t\t\t\tPunchCamera( 1.0f, 0.85f );\n\t\t\t\t\t\t// Swap back\n\t\t\t\t\t\tStartSwapTween( _swapAX, _swapAY, _swapBX, _swapBY );\n\t\t\t\t\t\t_phase = Phase.SwapBack;\n\t\t\t\t\t\t_phaseTimer = 0f;\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t_moves++;\n\t\t\t\t\t\t_comboCount = 1;\n\t\t\t\t\t\tBeginClear( matches );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase Phase.SwapBack:\n\t\t\t\tif ( _phaseTimer >= SwapDuration )\n\t\t\t\t{\n\t\t\t\t\t_phase = Phase.Idle;\n\t\t\t\t\t_phaseTimer = 0f;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase Phase.Clearing:\n\t\t\t\tif ( AllClearAnimsDone() )\n\t\t\t\t{\n\t\t\t\t\tDestroyClearedCandies();\n\t\t\t\t\t_phase = Phase.Collapsing;\n\t\t\t\t\t_phaseTimer = 0f;\n\t\t\t\t\tCollapseColumns();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase Phase.Collapsing:\n\t\t\t\tif ( AllAtAnchor() )\n\t\t\t\t{\n\t\t\t\t\t_phase = Phase.Refilling;\n\t\t\t\t\t_phaseTimer = 0f;\n\t\t\t\t\tRefill();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase Phase.Refilling:\n\t\t\t\tif ( AllAtAnchor() )\n\t\t\t\t{\n\t\t\t\t\t_phase = Phase.Pause;\n\t\t\t\t\t_phaseTimer = 0f;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase Phase.Pause:\n\t\t\t\tif ( _phaseTimer >= CascadePause )\n\t\t\t\t{\n\t\t\t\t\tvar matches = FindMatches();\n\t\t\t\t\tif ( matches.Count > 0 )\n\t\t\t\t\t{\n\t\t\t\t\t\t_comboCount++;\n\t\t\t\t\t\tBeginClear( matches );\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t_phase = Phase.Idle;\n\t\t\t\t\t\tCheckLevelEnd();\n\t\t\t\t\t\tif ( CurrentState == GameState.Playing && !HasPossibleMove() )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t// Levels rescue the player with a reshuffle; endless ends\n\t\t\t\t\t\t\t// precisely when there is nothing left to match.\n\t\t\t\t\t\t\tif ( IsEndless )\n\t\t\t\t\t\t\t\tEndEndlessRun();\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\tReshuffleBoardForAvailableMove();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tLog.Info( $\"Score: {_score} | Moves: {_moves}\" );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tvoid BeginClear( HashSet<int> matches )\n\t{\n\t\tbool activatingSpecial = ContainsSpecial( matches );\n\t\tint triggeredSpecials = CountSpecialsInCells( matches );\n\t\tvar rewardKind = SpecialKind.None;\n\t\tint rewardIdx = -1;\n\t\tif ( !activatingSpecial )\n\t\t\tTryChooseSpecialReward( matches, out rewardIdx, out rewardKind );\n\n\t\tExpandSpecialClears( matches );\n\t\tif ( rewardIdx >= 0 && matches.Count > 3 )\n\t\t\tmatches.Remove( rewardIdx );\n\n\t\tTrackObjectiveProgress( matches, rewardKind, triggeredSpecials );\n\t\t_score += matches.Count * 10 * _comboCount + (activatingSpecial ? 75 * _comboCount : 0);\n\t\tPlayUiSound( _comboCount > 1 ? ComboSound : ClearSound, MathF.Min( 1f, 0.55f + _comboCount * 0.12f ), 0.95f + _comboCount * 0.06f );\n\t\tvar center = Vector3.Zero;\n\t\tint centerCount = 0;\n\n\t\tif ( rewardIdx >= 0 && rewardKind != SpecialKind.None )\n\t\t{\n\t\t\tint sx = rewardIdx % Width;\n\t\t\tint sy = rewardIdx / Width;\n\t\t\tSetSpecialAt( sx, sy, rewardKind );\n\t\t\tvar specialVisual = _visuals[sx, sy];\n\t\t\tif ( specialVisual != null )\n\t\t\t{\n\t\t\t\tSpawnShockwave( specialVisual.Go.WorldPosition, new Color( 1f, 0.88f, 0.18f ), 0.52f, 72f );\n\t\t\t\tSpawnFireColumn( specialVisual.Go.WorldPosition, 0.65f );\n\t\t\t}\n\t\t}\n\n\t\tforeach ( var idx in matches )\n\t\t{\n\t\t\tint x = idx % Width;\n\t\t\tint y = idx / Width;\n\t\t\tvar v = _visuals[x, y];\n\t\t\tif ( v == null ) continue;\n\t\t\tv.Clearing = true;\n\t\t\tv.ClearTimer = ClearDuration;\n\t\t\t_specials[x, y] = SpecialKind.None;\n\t\t\tvar clearColor = PieceColor( v.Type );\n\t\t\tSpawnBurstAt( v.Go.WorldPosition, clearColor );\n\t\t\tSpawnShockwave( v.Go.WorldPosition, clearColor, 0.34f, 38f + _comboCount * 10f );\n\t\t\tcenter += v.Go.WorldPosition;\n\t\t\tcenterCount++;\n\t\t}\n\t\tif ( centerCount > 0 )\n\t\t{\n\t\t\tSpawnFireColumn( center / centerCount, MathF.Min( 1.8f, 1f + matches.Count * 0.05f + _comboCount * 0.18f ) );\n\t\t\tSpawnShockwave( center / centerCount, Color.White, 0.48f, 70f + matches.Count * 4f );\n\t\t}\n\t\t// Camera punch scales with combo size and chain depth\n\t\tfloat punchFov = MathF.Min( 8f, 2.5f + matches.Count * 0.3f + _comboCount * 1.2f );\n\t\tfloat punchShake = MathF.Min( 6f, 1.5f + _comboCount * 0.8f );\n\t\tPunchCamera( punchFov, punchShake );\n\t\t_phase = Phase.Clearing;\n\t\t_phaseTimer = 0f;\n\t}\n\n\tHashSet<int> GetSpecialActivationForSwap()\n\t{\n\t\tvar result = new HashSet<int>();\n\t\tAddSpecialCells( _swapAX, _swapAY, result );\n\t\tAddSpecialCells( _swapBX, _swapBY, result );\n\t\treturn result;\n\t}\n\n\tbool ContainsSpecial( HashSet<int> cells )\n\t{\n\t\tforeach ( var idx in cells )\n\t\t{\n\t\t\tint x = idx % Width;\n\t\t\tint y = idx / Width;\n\t\t\tif ( InBounds( x, y ) && _specials[x, y] != SpecialKind.None )\n\t\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tint CountSpecialsInCells( HashSet<int> cells )\n\t{\n\t\tint count = 0;\n\t\tforeach ( var idx in cells )\n\t\t{\n\t\t\tint x = idx % Width;\n\t\t\tint y = idx / Width;\n\t\t\tif ( InBounds( x, y ) && _specials[x, y] != SpecialKind.None )\n\t\t\t\tcount++;\n\t\t}\n\t\treturn count;\n\t}\n\n\tvoid TrackObjectiveProgress( HashSet<int> clearedCells, SpecialKind rewardKind, int triggeredSpecials )\n\t{\n\t\tif ( !HasSecondaryObjective )\n\t\t\treturn;\n\n\t\tvar level = CurrentLevelDefinition;\n\t\tint add = 0;\n\t\tswitch ( level.ObjectiveKind )\n\t\t{\n\t\t\tcase ColourBreakObjectiveKind.ClearPieces:\n\t\t\t\tadd = clearedCells.Count;\n\t\t\t\tbreak;\n\t\t\tcase ColourBreakObjectiveKind.ClearColour:\n\t\t\t\tforeach ( var idx in clearedCells )\n\t\t\t\t{\n\t\t\t\t\tint x = idx % Width;\n\t\t\t\t\tint y = idx / Width;\n\t\t\t\t\tif ( InBounds( x, y ) && _types[x, y] == level.ObjectiveType )\n\t\t\t\t\t\tadd++;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase ColourBreakObjectiveKind.MakeSpecials:\n\t\t\t\tadd = rewardKind == SpecialKind.None ? 0 : 1;\n\t\t\t\tbreak;\n\t\t\tcase ColourBreakObjectiveKind.TriggerSpecials:\n\t\t\t\tadd = triggeredSpecials;\n\t\t\t\tbreak;\n\t\t\tcase ColourBreakObjectiveKind.ChainCombos:\n\t\t\t\tadd = _comboCount > 1 ? 1 : 0;\n\t\t\t\tbreak;\n\t\t}\n\n\t\tif ( add > 0 )\n\t\t\t_objectiveProgress = Math.Min( level.ObjectiveTarget, _objectiveProgress + add );\n\t}\n\n\tvoid ExpandSpecialClears( HashSet<int> cells )\n\t{\n\t\tbool changed;\n\t\tint guard = 0;\n\t\tdo\n\t\t{\n\t\t\tchanged = false;\n\t\t\tvar snapshot = new List<int>( cells );\n\t\t\tforeach ( var idx in snapshot )\n\t\t\t{\n\t\t\t\tint before = cells.Count;\n\t\t\t\tAddSpecialCells( idx % Width, idx / Width, cells );\n\t\t\t\tif ( cells.Count > before ) changed = true;\n\t\t\t}\n\t\t}\n\t\twhile ( changed && ++guard < Width * Height );\n\t}\n\n\tvoid AddSpecialCells( int x, int y, HashSet<int> cells )\n\t{\n\t\tif ( !InBounds( x, y ) ) return;\n\t\tswitch ( _specials[x, y] )\n\t\t{\n\t\t\tcase SpecialKind.RowBlast:\n\t\t\t\tfor ( int cx = 0; cx < Width; cx++ ) cells.Add( y * Width + cx );\n\t\t\t\tbreak;\n\t\t\tcase SpecialKind.ColumnBlast:\n\t\t\t\tfor ( int cy = 0; cy < Height; cy++ ) cells.Add( cy * Width + x );\n\t\t\t\tbreak;\n\t\t\tcase SpecialKind.Bomb:\n\t\t\t\tfor ( int cy = y - 2; cy <= y + 2; cy++ )\n\t\t\t\t\tfor ( int cx = x - 2; cx <= x + 2; cx++ )\n\t\t\t\t\t\tif ( InBounds( cx, cy ) ) cells.Add( cy * Width + cx );\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tvoid TryChooseSpecialReward( HashSet<int> matches, out int rewardIdx, out SpecialKind kind )\n\t{\n\t\trewardIdx = -1;\n\t\tkind = SpecialKind.None;\n\t\tif ( matches.Count < 4 ) return;\n\n\t\tint preferred = matches.Contains( _swapBX + _swapBY * Width ) ? _swapBX + _swapBY * Width :\n\t\t\tmatches.Contains( _swapAX + _swapAY * Width ) ? _swapAX + _swapAY * Width :\n\t\t\tmatches.First();\n\n\t\trewardIdx = preferred;\n\t\tkind = matches.Count >= 5 ? SpecialKind.Bomb : SpecialKind.RowBlast;\n\n\t\tint x = preferred % Width;\n\t\tint y = preferred / Width;\n\t\tint sameRow = 0;\n\t\tint sameColumn = 0;\n\t\tforeach ( var idx in matches )\n\t\t{\n\t\t\tif ( idx / Width == y ) sameRow++;\n\t\t\tif ( idx % Width == x ) sameColumn++;\n\t\t}\n\t\tif ( matches.Count < 5 )\n\t\t\tkind = sameColumn > sameRow ? SpecialKind.ColumnBlast : SpecialKind.RowBlast;\n\t}\n\n\tvoid SetSpecialAt( int x, int y, SpecialKind kind )\n\t{\n\t\tif ( !InBounds( x, y ) ) return;\n\t\t_specials[x, y] = kind;\n\t\tvar v = _visuals[x, y];\n\t\tif ( v == null ) return;\n\t\tv.Special = kind;\n\t\tUpdateSpecialVisual( v );\n\t}\n\n\tvoid UpdateSpecialVisual( CandyVisual v )\n\t{\n\t\tif ( v?.SpecialBadgeRenderer == null ) return;\n\t\tv.SpecialBadgeRenderer.Tint = v.Special switch\n\t\t{\n\t\t\tSpecialKind.RowBlast => new Color( 1f, 0.86f, 0.12f, 1f ),\n\t\t\tSpecialKind.ColumnBlast => new Color( 0.22f, 0.92f, 1f, 1f ),\n\t\t\tSpecialKind.Bomb => new Color( 1f, 0.20f, 0.06f, 1f ),\n\t\t\t_ => new Color( 1f, 1f, 1f, 0f )\n\t\t};\n\t\tv.SpecialBadge.LocalRotation = v.Special switch\n\t\t{\n\t\t\tSpecialKind.ColumnBlast => Rotation.FromYaw( 90f ),\n\t\t\tSpecialKind.Bomb => Rotation.FromYaw( 45f ),\n\t\t\t_ => Rotation.Identity\n\t\t};\n\t\tv.SpecialBadge.LocalScale = v.Special == SpecialKind.Bomb\n\t\t\t? new Vector3( 0.22f, 0.04f, 0.22f )\n\t\t\t: v.Special == SpecialKind.None\n\t\t\t\t? Vector3.Zero\n\t\t\t\t: new Vector3( 0.44f, 0.035f, 0.06f );\n\t}\n\n\tvoid SpawnBurstAt( Vector3 pos, Color color )\n\t{\n\t\tif ( !EffectsEnabled ) return;\n\t\tint count = 24;\n\t\tfor ( int i = 0; i < count; i++ )\n\t\t{\n\t\t\tvar go = RentBurstObject( \"Colour Spark\" );\n\t\t\tgo.WorldPosition = pos + new Vector3( Game.Random.Float( -4f, 4f ), Game.Random.Float( -3f, 3f ), Game.Random.Float( -4f, 4f ) );\n\t\t\tgo.WorldRotation = Rotation.Random;\n\t\t\tbool hot = i % 3 != 0;\n\t\t\tfloat scale = hot ? Game.Random.Float( 0.09f, 0.18f ) : Game.Random.Float( 0.045f, 0.10f );\n\t\t\tgo.WorldScale = hot ? new Vector3( scale * 0.75f, scale * 0.22f, scale * 1.9f ) : Vector3.One * scale;\n\t\t\tvar mr = go.GetComponent<ModelRenderer>();\n\t\t\tmr.Model = hot ? (_boxModel ?? _sphereModel) : ((i % 2 == 0 && _sphereModel != null) ? _sphereModel : _boxModel);\n\t\t\tvar fireTint = i % 4 == 0 ? new Color( 1f, 0.18f, 0.02f ) : new Color( 1f, 0.70f, 0.10f );\n\t\t\tmr.Tint = hot ? Color.Lerp( fireTint, color, 0.28f ) : Color.Lerp( color, Color.White, 0.45f );\n\n\t\t\tfloat ang = (i / (float)count) * MathF.PI * 2f + Game.Random.Float( -0.4f, 0.4f );\n\t\t\tfloat speed = hot ? Game.Random.Float( 180f, 420f ) : Game.Random.Float( 320f, 620f );\n\t\t\tvar vel = new Vector3( MathF.Cos( ang ) * speed, Game.Random.Float( -180f, -30f ), Game.Random.Float( 260f, 650f ) + MathF.Sin( ang ) * speed * 0.25f );\n\n\t\t\t_bursts.Add( new Burst\n\t\t\t{\n\t\t\t\tGo = go,\n\t\t\t\tRenderer = mr,\n\t\t\t\tVelocity = vel,\n\t\t\t\tLife = hot ? 0.55f : 0.82f,\n\t\t\t\tMaxLife = hot ? 0.55f : 0.82f,\n\t\t\t\tBaseTint = mr.Tint,\n\t\t\t\tStartScale = scale,\n\t\t\t} );\n\t\t}\n\t}\n\n\tvoid SpawnFireColumn( Vector3 pos, float intensity )\n\t{\n\t\tif ( !EffectsEnabled ) return;\n\t\tint count = (int)(18 * intensity);\n\t\tfor ( int i = 0; i < count; i++ )\n\t\t{\n\t\t\tvar go = RentBurstObject( \"Match Fire\" );\n\t\t\tgo.WorldPosition = pos + new Vector3( Game.Random.Float( -18f, 18f ), Game.Random.Float( -8f, 4f ), Game.Random.Float( -8f, 8f ) );\n\t\t\tgo.WorldRotation = Rotation.Random;\n\t\t\tfloat scale = Game.Random.Float( 0.12f, 0.28f ) * intensity;\n\t\t\tgo.WorldScale = new Vector3( scale * 0.75f, scale * 0.16f, scale * Game.Random.Float( 2.2f, 3.8f ) );\n\t\t\tvar mr = go.GetComponent<ModelRenderer>();\n\t\t\tmr.Model = _boxModel ?? _sphereModel;\n\t\t\tmr.Tint = i % 3 == 0 ? new Color( 1f, 0.95f, 0.35f, 0.95f ) : new Color( 1f, 0.28f, 0.03f, 0.85f );\n\n\t\t\t_bursts.Add( new Burst\n\t\t\t{\n\t\t\t\tGo = go,\n\t\t\t\tRenderer = mr,\n\t\t\t\tVelocity = new Vector3( Game.Random.Float( -80f, 80f ), Game.Random.Float( -60f, 10f ), Game.Random.Float( 520f, 920f ) * intensity ),\n\t\t\t\tLife = Game.Random.Float( 0.42f, 0.72f ),\n\t\t\t\tMaxLife = 0.72f,\n\t\t\t\tBaseTint = mr.Tint,\n\t\t\t\tStartScale = scale,\n\t\t\t} );\n\t\t}\n\n\t\tvar lightGo = new GameObject( true, \"Match Fire Light\" );\n\t\tlightGo.Flags = GameObjectFlags.NotNetworked;\n\t\tlightGo.WorldPosition = pos + new Vector3( 0f, -18f, 22f );\n\t\tvar light = lightGo.AddComponent<PointLight>();\n\t\tlight.LightColor = new Color( 1f, 0.42f, 0.08f );\n\t\tlight.Radius = 360f * intensity;\n\t\t_bursts.Add( new Burst\n\t\t{\n\t\t\tGo = lightGo,\n\t\t\tRenderer = null,\n\t\t\tVelocity = Vector3.Zero,\n\t\t\tLife = 0.34f,\n\t\t\tMaxLife = 0.34f,\n\t\t\tBaseTint = Color.White,\n\t\t\tStartScale = 1f,\n\t\t} );\n\t}\n\n\tvoid AnimateBursts()\n\t{\n\t\tfloat dt = Time.Delta;\n\t\tfor ( int i = _bursts.Count - 1; i >= 0; i-- )\n\t\t{\n\t\t\tvar b = _bursts[i];\n\t\t\tb.Life -= dt;\n\t\t\tif ( b.Life <= 0f || b.Go == null )\n\t\t\t{\n\t\t\t\t// Renderer bursts go back to the pool; the lone PointLight burst is destroyed.\n\t\t\t\tif ( b.Go != null && b.Renderer != null )\n\t\t\t\t\tReturnBurstObject( b.Go );\n\t\t\t\telse\n\t\t\t\t\tb.Go?.Destroy();\n\t\t\t\t_bursts.RemoveAt( i );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// gravity + drag\n\t\t\tb.Velocity.z -= 1400f * dt;\n\t\t\tb.Velocity *= 1f - dt * 1.4f;\n\t\t\tb.Go.WorldPosition += b.Velocity * dt;\n\t\t\tb.Go.WorldRotation *= Rotation.From( new Angles( dt * 540f, dt * 720f, dt * 360f ) );\n\n\t\t\tfloat k = b.Life / b.MaxLife;\n\t\t\tif ( b.Renderer != null )\n\t\t\t{\n\t\t\t\tb.Go.WorldScale *= 1f + dt * 0.55f;\n\t\t\t\tvar t = b.BaseTint;\n\t\t\t\tt.a = k;\n\t\t\t\tb.Renderer.Tint = t;\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid SpawnShockwave( Vector3 pos, Color color, float life, float radius )\n\t{\n\t\tif ( !EffectsEnabled ) return;\n\t\tvar go = new GameObject( true, \"ColourBreak Shockwave\" );\n\t\tgo.Flags = GameObjectFlags.NotNetworked;\n\t\tgo.WorldPosition = pos + new Vector3( 0f, -3f, 0f );\n\t\tgo.WorldScale = new Vector3( 0.08f, 0.01f, 0.08f );\n\t\tvar mr = go.AddComponent<ModelRenderer>();\n\t\tmr.Model = _sphereModel ?? _boxModel;\n\t\tmr.Tint = new Color( color.r, color.g, color.b, 0.55f );\n\t\t_shockwaves.Add( new Shockwave\n\t\t{\n\t\t\tGo = go,\n\t\t\tRenderer = mr,\n\t\t\tTint = mr.Tint,\n\t\t\tLife = life,\n\t\t\tMaxLife = life,\n\t\t\tRadius = radius,\n\t\t} );\n\t}\n\n\tvoid AnimateShockwaves()\n\t{\n\t\tfloat dt = Time.Delta;\n\t\tfor ( int i = _shockwaves.Count - 1; i >= 0; i-- )\n\t\t{\n\t\t\tvar wave = _shockwaves[i];\n\t\t\twave.Life -= dt;\n\t\t\tif ( wave.Life <= 0f || wave.Go == null )\n\t\t\t{\n\t\t\t\twave.Go?.Destroy();\n\t\t\t\t_shockwaves.RemoveAt( i );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfloat k = 1f - wave.Life / wave.MaxLife;\n\t\t\tfloat eased = EaseOutCubic( k );\n\t\t\tfloat scale = MathX.Lerp( 0.04f, wave.Radius / 50f, eased );\n\t\t\twave.Go.WorldScale = new Vector3( scale, 0.012f, scale );\n\t\t\tvar tint = wave.Tint;\n\t\t\ttint.a = 0.55f * (1f - k);\n\t\t\twave.Renderer.Tint = tint;\n\t\t}\n\t}\n\n\tbool AllClearAnimsDone()\n\t{\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t{\n\t\t\t\tvar v = _visuals[x, y];\n\t\t\t\tif ( v != null && v.Clearing && v.ClearTimer > 0f ) return false;\n\t\t\t}\n\t\treturn true;\n\t}\n\n\tvoid DestroyClearedCandies()\n\t{\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t{\n\t\t\t\tvar v = _visuals[x, y];\n\t\t\t\tif ( v != null && v.Clearing )\n\t\t\t\t{\n\t\t\t\t\tv.Go?.Destroy();\n\t\t\t\t\t_visuals[x, y] = null;\n\t\t\t\t\t_types[x, y] = Empty;\n\t\t\t\t}\n\t\t\t}\n\t}\n\n\tbool AllAtAnchor()\n\t{\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t{\n\t\t\t\tvar v = _visuals[x, y];\n\t\t\t\tif ( v == null ) continue;\n\t\t\t\tif ( v.Spawning ) return false;\n\t\t\t\tif ( (v.Go.WorldPosition - v.Anchor).LengthSquared > 4f ) return false;\n\t\t\t}\n\t\treturn true;\n\t}\n\n\tvoid CollapseColumns()\n\t{\n\t\tfor ( int x = 0; x < Width; x++ )\n\t\t{\n\t\t\tint writeY = 0;\n\t\t\tfor ( int y = 0; y < Height; y++ )\n\t\t\t{\n\t\t\t\tif ( _types[x, y] == Empty ) continue;\n\t\t\t\tif ( y != writeY )\n\t\t\t\t{\n\t\t\t\t\t_types[x, writeY] = _types[x, y];\n\t\t\t\t\t_types[x, y] = Empty;\n\t\t\t\t\t_specials[x, writeY] = _specials[x, y];\n\t\t\t\t\t_specials[x, y] = SpecialKind.None;\n\t\t\t\t\t_visuals[x, writeY] = _visuals[x, y];\n\t\t\t\t\t_visuals[x, y] = null;\n\t\t\t\t\tvar v = _visuals[x, writeY];\n\t\t\t\t\tif ( v != null )\n\t\t\t\t\t{\n\t\t\t\t\t\tv.Anchor = CellToWorld( x, writeY );\n\t\t\t\t\t\tv.Special = _specials[x, writeY];\n\t\t\t\t\t\tUpdateSpecialVisual( v );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twriteY++;\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid Refill()\n\t{\n\t\tfor ( int x = 0; x < Width; x++ )\n\t\t{\n\t\t\tint spawnIndex = 0;\n\t\t\tfor ( int y = 0; y < Height; y++ )\n\t\t\t{\n\t\t\t\tif ( _types[x, y] != Empty ) continue;\n\t\t\t\tint type = Game.Random.Int( 0, CandyTypeCount - 1 );\n\t\t\t\t_specials[x, y] = SpecialKind.None;\n\t\t\t\tSpawnCandy( x, y, type, instant: false );\n\t\t\t\t// Stagger spawn position so they form a falling stream\n\t\t\t\tvar v = _visuals[x, y];\n\t\t\t\tif ( v != null )\n\t\t\t\t{\n\t\t\t\t\tvar p = v.Go.WorldPosition;\n\t\t\t\t\tp.z += spawnIndex * CellSpacing * 0.6f;\n\t\t\t\t\tv.Go.WorldPosition = p;\n\t\t\t\t}\n\t\t\t\tspawnIndex++;\n\t\t\t}\n\t\t}\n\t}\n\n\tHashSet<int> FindMatches()\n\t{\n\t\tvar result = new HashSet<int>();\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t{\n\t\t\tint runStart = 0;\n\t\t\tfor ( int x = 1; x <= Width; x++ )\n\t\t\t{\n\t\t\t\tbool atEnd = x == Width;\n\t\t\t\tif ( !atEnd && _types[x, y] == _types[runStart, y] && _types[x, y] != Empty ) continue;\n\t\t\t\tint runLen = x - runStart;\n\t\t\t\tif ( _types[runStart, y] != Empty && runLen >= 3 )\n\t\t\t\t\tfor ( int rx = runStart; rx < x; rx++ ) result.Add( y * Width + rx );\n\t\t\t\trunStart = x;\n\t\t\t}\n\t\t}\n\t\tfor ( int x = 0; x < Width; x++ )\n\t\t{\n\t\t\tint runStart = 0;\n\t\t\tfor ( int y = 1; y <= Height; y++ )\n\t\t\t{\n\t\t\t\tbool atEnd = y == Height;\n\t\t\t\tif ( !atEnd && _types[x, y] == _types[x, runStart] && _types[x, y] != Empty ) continue;\n\t\t\t\tint runLen = y - runStart;\n\t\t\t\tif ( _types[x, runStart] != Empty && runLen >= 3 )\n\t\t\t\t\tfor ( int ry = runStart; ry < y; ry++ ) result.Add( ry * Width + x );\n\t\t\t\trunStart = y;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\tbool HasPossibleMove()\n\t{\n\t\tif ( _types == null ) return false;\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t{\n\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t{\n\t\t\t\tif ( _specials != null && _specials[x, y] != SpecialKind.None )\n\t\t\t\t\treturn true;\n\t\t\t\tif ( x + 1 < Width && SwapWouldMatch( x, y, x + 1, y ) ) return true;\n\t\t\t\tif ( y + 1 < Height && SwapWouldMatch( x, y, x, y + 1 ) ) return true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\tbool SwapWouldMatch( int x1, int y1, int x2, int y2 )\n\t{\n\t\t(_types[x1, y1], _types[x2, y2]) = (_types[x2, y2], _types[x1, y1]);\n\t\tbool result = IsMatchAt( x1, y1 ) || IsMatchAt( x2, y2 );\n\t\t(_types[x1, y1], _types[x2, y2]) = (_types[x2, y2], _types[x1, y1]);\n\t\treturn result;\n\t}\n\n\tbool IsMatchAt( int x, int y )\n\t{\n\t\tint type = _types[x, y];\n\t\tif ( type == Empty ) return false;\n\n\t\tint count = 1;\n\t\tfor ( int cx = x - 1; cx >= 0 && _types[cx, y] == type; cx-- ) count++;\n\t\tfor ( int cx = x + 1; cx < Width && _types[cx, y] == type; cx++ ) count++;\n\t\tif ( count >= 3 ) return true;\n\n\t\tcount = 1;\n\t\tfor ( int cy = y - 1; cy >= 0 && _types[x, cy] == type; cy-- ) count++;\n\t\tfor ( int cy = y + 1; cy < Height && _types[x, cy] == type; cy++ ) count++;\n\t\treturn count >= 3;\n\t}\n\n\tvoid ReshuffleBoardForAvailableMove()\n\t{\n\t\tvar types = new List<int>( Width * Height );\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t\tif ( _types[x, y] != Empty ) types.Add( _types[x, y] );\n\n\t\tfor ( int attempt = 0; attempt < 40; attempt++ )\n\t\t{\n\t\t\tShuffleList( types );\n\t\t\tint i = 0;\n\t\t\tfor ( int y = 0; y < Height; y++ )\n\t\t\t{\n\t\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t\t{\n\t\t\t\t\t_types[x, y] = types[i++ % types.Count];\n\t\t\t\t\t_specials[x, y] = SpecialKind.None;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( FindMatches().Count == 0 && HasPossibleMove() )\n\t\t\t\tbreak;\n\t\t}\n\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t{\n\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t{\n\t\t\t\tvar v = _visuals[x, y];\n\t\t\t\tif ( v == null ) continue;\n\t\t\t\tv.Type = _types[x, y];\n\t\t\t\tv.Special = SpecialKind.None;\n\t\t\t\tv.Renderer.Model = ModelForType( v.Type );\n\t\t\t\tApplyPieceVisualStyle( v );\n\t\t\t\tUpdateSpecialVisual( v );\n\t\t\t}\n\t\t}\n\n\t\tSpawnShockwave( BoardOrigin, new Color( 0.25f, 0.92f, 1f ), 0.7f, 150f );\n\t\tPunchCamera( 2f, 1.2f );\n\t\tLog.Info( \"Board reshuffled: no available moves.\" );\n\t}\n\n\tvoid EndEndlessRun()\n\t{\n\t\tCurrentState = GameState.Lost;\n\t\tEndlessRecordSet = ColourBreakProgressStore.RecordEndlessScore( _saveData, _score );\n\n\t\ttry\n\t\t{\n\t\t\tSandbox.Services.Stats.Increment( \"endless-runs\", 1 );\n\t\t\tSandbox.Services.Stats.SetValue( \"endless-best\", EndlessBest, \"\", null );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Info( $\"Leaderboard stats unavailable: {e.Message}\" );\n\t\t}\n\n\t\tPlayUiSound( DeniedSound, 0.85f, 0.72f );\n\t\tSpawnShockwave( BoardOrigin, new Color( 1f, 0.18f, 0.06f ), 0.7f, 150f );\n\t\tPunchCamera( 3f, 2.4f );\n\t}\n\n\tvoid CheckLevelEnd()\n\t{\n\t\tif ( CurrentState != GameState.Playing ) return;\n\t\tif ( IsEndless ) return; // endless ends only on a dead board\n\t\tif ( _score >= TargetScore && ObjectiveComplete )\n\t\t{\n\t\t\tCurrentState = GameState.Won;\n\t\t\tSaveWinProgress();\n\t\t\tPlayUiSound( ComboSound, 1f, 1.18f );\n\t\t\tSpawnFireColumn( BoardOrigin, 1.4f );\n\t\t\tSpawnShockwave( BoardOrigin, new Color( 1f, 0.86f, 0.12f ), 0.85f, 190f );\n\t\t\tPunchCamera( 5f, 3.2f );\n\t\t\treturn;\n\t\t}\n\t\tif ( _moves >= MoveLimit )\n\t\t{\n\t\t\tCurrentState = GameState.Lost;\n\t\t\tPlayUiSound( DeniedSound, 0.85f, 0.72f );\n\t\t\tSpawnShockwave( BoardOrigin, new Color( 1f, 0.18f, 0.06f ), 0.7f, 130f );\n\t\t\tPunchCamera( 2.5f, 2.0f );\n\t\t}\n\t}\n\n\tvoid ShuffleList( List<int> list )\n\t{\n\t\tfor ( int i = list.Count - 1; i > 0; i-- )\n\t\t{\n\t\t\tint j = Game.Random.Int( 0, i );\n\t\t\t(list[i], list[j]) = (list[j], list[i]);\n\t\t}\n\t}\n\n\tint CountSpecials()\n\t{\n\t\tif ( _specials == null ) return 0;\n\t\tint count = 0;\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t\tif ( _specials[x, y] != SpecialKind.None ) count++;\n\t\treturn count;\n\t}\n\n\t// -------------------- visuals --------------------\n\n\tvoid AnimateCandies()\n\t{\n\t\tif ( _visuals == null ) return;\n\t\tfloat dt = Time.Delta;\n\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t{\n\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t{\n\t\t\t\tvar v = _visuals[x, y];\n\t\t\t\tif ( v == null || v.Go == null ) continue;\n\n\t\t\t\tint t = v.Type;\n\t\t\t\tvar baseShape = PieceShape( t );\n\t\t\t\tfloat baseScale = CandyScale;\n\n\t\t\t\t// 1) Clearing animation: shrink + spin fast\n\t\t\t\tif ( v.Clearing )\n\t\t\t\t{\n\t\t\t\t\tv.ClearTimer -= dt;\n\t\t\t\t\tfloat k = MathF.Max( 0f, v.ClearTimer / ClearDuration );\n\t\t\t\t\tfloat s = baseScale * EaseOutBack( k ) * 1.4f * k;\n\t\t\t\t\tv.Go.WorldScale = baseShape * s;\n\t\t\t\t\tv.Go.WorldRotation *= Rotation.FromYaw( dt * 720f );\n\t\t\t\t\tv.Renderer.Tint = Color.Lerp( PieceColor( t ), Color.White, 1f - k );\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// 2) Swap tween\n\t\t\t\tif ( v.Swapping )\n\t\t\t\t{\n\t\t\t\t\tv.SwapT += dt / SwapDuration;\n\t\t\t\t\tfloat k = MathF.Min( 1f, v.SwapT );\n\t\t\t\t\tfloat e = EaseInOutCubic( k );\n\t\t\t\t\tvar pos = Vector3.Lerp( v.SwapFrom, v.SwapTo, e );\n\t\t\t\t\t// Lift slightly off the board for an arc\n\t\t\t\t\tpos.y -= MathF.Sin( k * MathF.PI ) * CellSpacing * 0.18f;\n\t\t\t\t\tv.Go.WorldPosition = pos;\n\t\t\t\t\tif ( k >= 1f ) { v.Swapping = false; v.Go.WorldPosition = v.SwapTo; v.Anchor = v.SwapTo; }\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t// 3) Falling toward anchor (used for collapse + refill)\n\t\t\t\t\tvar pos = v.Go.WorldPosition;\n\t\t\t\t\tvar to = v.Anchor;\n\t\t\t\t\tvar diff = to - pos;\n\t\t\t\t\tif ( diff.LengthSquared > 1f )\n\t\t\t\t\t{\n\t\t\t\t\t\t// gravity-ish acceleration toward anchor on Z (vertical)\n\t\t\t\t\t\tv.Velocity.z -= 4500f * dt;\n\t\t\t\t\t\tpos.z += v.Velocity.z * dt;\n\t\t\t\t\t\tpos.x = MathX.Lerp( pos.x, to.x, dt * 14f );\n\t\t\t\t\t\tpos.y = MathX.Lerp( pos.y, to.y, dt * 14f );\n\n\t\t\t\t\t\tif ( pos.z <= to.z )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tpos.z = to.z;\n\t\t\t\t\t\t\t// Squash on landing\n\t\t\t\t\t\t\tv.Velocity.z = 0f;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tv.Go.WorldPosition = pos;\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tv.Go.WorldPosition = to;\n\t\t\t\t\t\tv.Velocity = Vector3.Zero;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// 4) Spawn pop-in\n\t\t\t\tfloat spawnScale = 1f;\n\t\t\t\tif ( v.Spawning )\n\t\t\t\t{\n\t\t\t\t\tv.SpawnTimer -= dt;\n\t\t\t\t\tfloat k = MathF.Max( 0f, v.SpawnTimer / 0.25f );\n\t\t\t\t\tspawnScale = MathX.Lerp( 1f, 0.4f, k );\n\t\t\t\t\tif ( v.SpawnTimer <= 0f ) v.Spawning = false;\n\t\t\t\t}\n\n\t\t\t\t// 5) Idle bob + gentle spin\n\t\t\t\tv.BobPhase += dt;\n\t\t\t\tfloat bob = MathF.Sin( v.BobPhase * 2.4f ) * 1.2f;\n\t\t\t\tif ( !v.Swapping && (v.Go.WorldPosition - v.Anchor).LengthSquared < 4f )\n\t\t\t\t{\n\t\t\t\t\tvar p = v.Anchor;\n\t\t\t\t\tp.y -= bob;\n\t\t\t\t\tv.Go.WorldPosition = p;\n\t\t\t\t}\n\t\t\t\tv.Go.WorldRotation *= Rotation.FromYaw( dt * (10f + (t * 3f)) );\n\n\t\t\t\t// 6) Final scale (with highlight applied later in ApplyHighlights)\n\t\t\t\tv.Go.WorldScale = baseShape * baseScale * spawnScale;\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid ApplyHighlights()\n\t{\n\t\tif ( _visuals == null ) return;\n\t\tfor ( int y = 0; y < Height; y++ )\n\t\t{\n\t\t\tfor ( int x = 0; x < Width; x++ )\n\t\t\t{\n\t\t\t\tvar v = _visuals[x, y];\n\t\t\t\tif ( v == null || v.Renderer == null || v.Clearing ) continue;\n\n\t\t\t\tvar baseTint = PieceColor( v.Type );\n\t\t\t\tbool isCursor = x == _cursorX && y == _cursorY;\n\t\t\t\tbool isSelected = _hasSelection && x == _selectedX && y == _selectedY;\n\t\t\t\tbool isLegalNeighbour = _hasSelection && !isSelected && AreAdjacent( _selectedX, _selectedY, x, y );\n\t\t\t\tbool isHint = _hasHint && ((x == _hintAX && y == _hintAY) || (x == _hintBX && y == _hintBY));\n\n\t\t\t\tfloat scaleMul = 1f;\n\t\t\t\tif ( isHint && !isCursor && !isSelected )\n\t\t\t\t{\n\t\t\t\t\tfloat p = 0.5f + 0.5f * MathF.Sin( Time.Now * 7f );\n\t\t\t\t\tbaseTint = Color.Lerp( baseTint, Color.White, 0.30f + 0.45f * p );\n\t\t\t\t\tscaleMul += 0.18f + 0.16f * p;\n\t\t\t\t}\n\t\t\t\tif ( isCursor )\n\t\t\t\t{\n\t\t\t\t\tfloat p = 0.5f + 0.5f * MathF.Sin( Time.Now * 12f );\n\t\t\t\t\t// Instead of a box, pulse the candy itself significantly brighter and bigger\n\t\t\t\t\tbaseTint = Color.Lerp( baseTint, Color.White, 0.40f + 0.40f * p );\n\t\t\t\t\tscaleMul += 0.25f + 0.15f * p;\n\t\t\t\t}\n\t\t\t\tif ( isSelected )\n\t\t\t\t{\n\t\t\t\t\t// Selection gets a distinct yellow overlay\n\t\t\t\t\tbaseTint = Color.Lerp( baseTint, new Color( 1f, 0.95f, 0.2f ), 0.60f );\n\t\t\t\t\tscaleMul += 0.35f;\n\t\t\t\t}\n\t\t\t\tif ( isLegalNeighbour )\n\t\t\t\t{\n\t\t\t\t\tfloat p = 0.5f + 0.5f * MathF.Sin( Time.Now * 9f );\n\t\t\t\t\tbaseTint = Color.Lerp( baseTint, new Color( 0.22f, 0.92f, 1f ), 0.18f + 0.14f * p );\n\t\t\t\t\tscaleMul += 0.08f + 0.05f * p;\n\t\t\t\t}\n\n\t\t\t\tv.Renderer.Tint = baseTint;\n\t\t\t\tif ( v.RimRenderer != null )\n\t\t\t\t\tv.RimRenderer.Tint = Color.Lerp( baseTint, Color.Black, isSelected ? 0.12f : isLegalNeighbour ? 0.22f : 0.36f );\n\t\t\t\tif ( v.FaceRenderer != null )\n\t\t\t\t\tv.FaceRenderer.Tint = Color.Lerp( baseTint, Color.White, isCursor || isSelected || isLegalNeighbour ? 0.45f : 0.20f );\n\t\t\t\tif ( v.AccentRenderer != null )\n\t\t\t\t\tv.AccentRenderer.Tint = Color.Lerp( baseTint, Color.White, isCursor || isSelected || isLegalNeighbour ? 0.82f : 0.52f );\n\t\t\t\tif ( v.ShineRenderer != null )\n\t\t\t\t\tv.ShineRenderer.Tint = isCursor || isSelected || isLegalNeighbour ? new Color( 1f, 1f, 1f, 1f ) : new Color( 1f, 1f, 1f, 0.74f );\n\t\t\t\tif ( v.Halo != null )\n\t\t\t\t{\n\t\t\t\t\tfloat p = 0.5f + 0.5f * MathF.Sin( Time.Now * 10f );\n\t\t\t\t\tif ( isSelected )\n\t\t\t\t\t{\n\t\t\t\t\t\tv.Halo.LocalScale = new Vector3( 1.34f + 0.05f * p, 0.055f, 1.34f + 0.05f * p );\n\t\t\t\t\t\tif ( v.HaloRenderer != null ) v.HaloRenderer.Tint = new Color( 1f, 0.86f, 0.08f, 0.90f );\n\t\t\t\t\t}\n\t\t\t\t\telse if ( isCursor )\n\t\t\t\t\t{\n\t\t\t\t\t\tv.Halo.LocalScale = new Vector3( 1.20f + 0.05f * p, 0.040f, 1.20f + 0.05f * p );\n\t\t\t\t\t\tif ( v.HaloRenderer != null ) v.HaloRenderer.Tint = new Color( 1f, 1f, 1f, 0.68f );\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tv.Halo.LocalScale = Vector3.Zero;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif ( v.Guide != null )\n\t\t\t\t{\n\t\t\t\t\tfloat p = 0.5f + 0.5f * MathF.Sin( Time.Now * 8f );\n\t\t\t\t\tif ( isLegalNeighbour )\n\t\t\t\t\t{\n\t\t\t\t\t\tv.Guide.LocalScale = new Vector3( 1.16f + 0.07f * p, 0.030f, 1.16f + 0.07f * p );\n\t\t\t\t\t\tif ( v.GuideRenderer != null ) v.GuideRenderer.Tint = new Color( 0.22f, 0.92f, 1f, 0.62f );\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tv.Guide.LocalScale = Vector3.Zero;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif ( !v.Swapping && !v.Spawning )\n\t\t\t\t\tv.Go.WorldScale = PieceShape( v.Type ) * CandyScale * scaleMul;\n\t\t\t}\n\t\t}\n\t}\n\n\tint PickTypeAvoidingImmediateMatch( int x, int y )\n\t{\n\t\tfor ( int attempt = 0; attempt < 12; attempt++ )\n\t\t{\n\t\t\tint type = Game.Random.Int( 0, CandyTypeCount - 1 );\n\t\t\tbool badH = x >= 2 && _types[x - 1, y] == type && _types[x - 2, y] == type;\n\t\t\tbool badV = y >= 2 && _types[x, y - 1] == type && _types[x, y - 2] == type;\n\t\t\tif ( !badH && !badV ) return type;\n\t\t}\n\t\treturn Game.Random.Int( 0, CandyTypeCount - 1 );\n\t}\n\n\t// -------------------- helpers --------------------\n\n\tbool AreAdjacent( int x1, int y1, int x2, int y2 ) => Math.Abs( x1 - x2 ) + Math.Abs( y1 - y2 ) == 1;\n\tbool InBounds( int x, int y ) => x >= 0 && x < Width && y >= 0 && y < Height;\n\n\tbool PressedEither( string a, string b ) => Pressed( a ) || Pressed( b );\n\n\tbool ActionPressed( params string[] actions )\n\t{\n\t\tforeach ( var action in actions )\n\t\t\tif ( Input.Pressed( action ) )\n\t\t\t\treturn true;\n\n\t\treturn false;\n\t}\n\n\tbool Pressed( string key )\n\t{\n\t\tbool down = Input.Keyboard.Down( key );\n\t\t_prevKeys.TryGetValue( key, out bool prev );\n\t\t_prevKeys[key] = down;\n\t\treturn down && !prev;\n\t}\n\n\tvoid ClearBoardObjects()\n\t{\n\t\tif ( _visuals == null ) return;\n\t\tfor ( int y = 0; y < _visuals.GetLength( 1 ); y++ )\n\t\t\tfor ( int x = 0; x < _visuals.GetLength( 0 ); x++ )\n\t\t\t\t_visuals[x, y]?.Go?.Destroy();\n\t\tforeach ( var burst in _bursts )\n\t\t{\n\t\t\tif ( burst.Go != null && burst.Renderer != null )\n\t\t\t\tReturnBurstObject( burst.Go );\n\t\t\telse\n\t\t\t\tburst.Go?.Destroy();\n\t\t}\n\t\tforeach ( var wave in _shockwaves ) wave.Go?.Destroy();\n\t\t_bursts.Clear();\n\t\t_shockwaves.Clear();\n\t}\n\n\tvoid CleanupDecor()\n\t{\n\t\tforeach ( var go in _decor ) go?.Destroy();\n\t\t_decor.Clear();\n\t}\n\n\tbool TryGetMainCamera( out CameraComponent cam )\n\t{\n\t\tcam = null;\n\t\tif ( Scene == null ) return false;\n\t\tforeach ( var c in Scene.GetAllComponents<CameraComponent>() )\n\t\t{\n\t\t\tif ( c.IsMainCamera ) { cam = c; return true; }\n\t\t\tcam ??= c;\n\t\t}\n\t\treturn cam != null;\n\t}\n\n\tvoid FrameMainCameraToBoard()\n\t{\n\t\tif ( !TryGetMainCamera( out var cam ) || cam.GameObject == null ) return;\n\t\tfloat boardW = Width * CellSpacing;\n\t\tfloat boardH = Height * CellSpacing;\n\t\tfloat verticalFov = 44f;\n\t\tfloat verticalFovRad = verticalFov * MathF.PI / 180f;\n\t\tfloat aspect = 16f / 9f;\n\t\tfloat targetW = boardW + CellSpacing * 4.8f;\n\t\tfloat targetH = boardH + CellSpacing * 6.4f;\n\t\tfloat distanceForHeight = (targetH * 0.5f) / MathF.Tan( verticalFovRad * 0.5f );\n\t\tfloat horizontalFovRad = 2f * MathF.Atan( MathF.Tan( verticalFovRad * 0.5f ) * aspect );\n\t\tfloat distanceForWidth = (targetW * 0.5f) / MathF.Tan( horizontalFovRad * 0.5f );\n\t\tfloat distance = MathF.Max( distanceForHeight, distanceForWidth ) * 1.45f;\n\t\tvar center = BoardOrigin + new Vector3( 0f, 2f, 0f );\n\t\tvar camPos = center + new Vector3( 0f, -distance, 0f );\n\t\tcam.GameObject.WorldPosition = camPos;\n\t\tcam.GameObject.WorldRotation = Rotation.LookAt( center - camPos, Vector3.Up );\n\t\tcam.FieldOfView = verticalFov;\n\t\tcam.BackgroundColor = new Color( 0.055f, 0.045f, 0.095f );\n\t\tcam.EnablePostProcessing = true;\n\t\tcam.ZNear = 1f;\n\t\tcam.ZFar = MathF.Max( cam.ZFar, 20000f );\n\t\t_activeCamera = cam;\n\t\t_baseFov = cam.FieldOfView;\n\t\t_baseCameraPos = cam.GameObject.WorldPosition;\n\t}\n\n\tvoid UpdateAmbience()\n\t{\n\t\tfloat t = Time.Now;\n\t\tif ( _orbitLightL != null )\n\t\t{\n\t\t\tfloat r = MathF.Max( Width, Height ) * CellSpacing * 0.7f;\n\t\t\t_orbitLightL.WorldPosition = BoardOrigin + new Vector3( MathF.Cos( t * 0.7f ) * r, -CellSpacing * 1.2f, MathF.Sin( t * 0.7f ) * r );\n\t\t}\n\t\tif ( _orbitLightR != null )\n\t\t{\n\t\t\tfloat r = MathF.Max( Width, Height ) * CellSpacing * 0.7f;\n\t\t\t_orbitLightR.WorldPosition = BoardOrigin + new Vector3( MathF.Cos( t * 0.7f + MathF.PI ) * r, -CellSpacing * 1.2f, MathF.Sin( t * 0.7f + MathF.PI ) * r );\n\t\t}\n\n\t\tif ( _trayInnerRenderer != null )\n\t\t{\n\t\t\tfloat r = 0.05f + 0.025f * MathF.Sin( t * 0.55f );\n\t\t\tfloat g = 0.07f + 0.025f * MathF.Sin( t * 0.55f + 2f );\n\t\t\tfloat b = 0.12f + 0.035f * MathF.Sin( t * 0.55f + 4f );\n\t\t\tvar shift = new Color( r, g, b );\n\t\t\t_trayInnerRenderer.Tint = Color.Lerp( _trayBaseColor, shift, 0.45f );\n\t\t}\n\n\t}\n\n\tvoid UpdateCameraPunch()\n\t{\n\t\tif ( _activeCamera == null ) return;\n\t\tfloat dt = Time.Delta;\n\t\t_fovPunch = MathX.Lerp( _fovPunch, 0f, dt * 6f );\n\t\t_shakeAmount = MathX.Lerp( _shakeAmount, 0f, dt * 5f );\n\t\t_activeCamera.FieldOfView = _baseFov + _fovPunch;\n\n\t\tif ( _shakeAmount > 0.01f && _activeCamera.GameObject != null )\n\t\t{\n\t\t\tvar offset = new Vector3(\n\t\t\t\tGame.Random.Float( -1f, 1f ),\n\t\t\t\tGame.Random.Float( -1f, 1f ),\n\t\t\t\tGame.Random.Float( -1f, 1f ) ) * _shakeAmount;\n\t\t\t_activeCamera.GameObject.WorldPosition = _baseCameraPos + offset;\n\t\t}\n\t\telse if ( _activeCamera.GameObject != null )\n\t\t{\n\t\t\t_activeCamera.GameObject.WorldPosition = _baseCameraPos;\n\t\t}\n\t}\n\n\tvoid PunchCamera( float fovDelta, float shake )\n\t{\n\t\tif ( !CameraShakeEnabled ) return;\n\t\t_fovPunch -= fovDelta; // negative FOV = zoom in\n\t\t_shakeAmount = MathF.Max( _shakeAmount, shake );\n\t}\n\n\t// Seamless-ish looping background music. Re-triggers when the clip ends; the\n\t// synthesized loop wraps at matching amplitude so the seam isn't audible.\n\tvoid UpdateMusic()\n\t{\n\t\tif ( !MusicEnabled || string.IsNullOrWhiteSpace( MusicSound ) )\n\t\t{\n\t\t\tif ( _music.IsValid() )\n\t\t\t{\n\t\t\t\t_music.Stop( 0.4f );\n\t\t\t\t_music = default;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif ( !_music.IsValid() || _music.IsStopped || _music.Finished )\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\t_music = Sound.Play( MusicSound );\n\t\t\t\tif ( _music.IsValid() )\n\t\t\t\t{\n\t\t\t\t\t_music.SpacialBlend = 0f;\n\t\t\t\t\t_music.Volume = MusicVolume;\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch\n\t\t\t{\n\t\t\t\t// Music asset missing/unsupported \u2014 silently continue without it.\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t_music.Volume = MusicVolume;\n\t\t}\n\t}\n\n\tvoid PlayUiSound( string sound, float volume, float pitch )\n\t{\n\t\tif ( !AudioEnabled ) return;\n\t\tif ( string.IsNullOrWhiteSpace( sound ) || _audioCooldown > 0f ) return;\n\t\ttry\n\t\t{\n\t\t\tvar handle = Sound.Play( sound, 0f );\n\t\t\thandle.Volume = volume;\n\t\t\thandle.Pitch = pitch;\n\t\t\thandle.SpacialBlend = 0f;\n\t\t\t_audioCooldown = 0.035f;\n\t\t}\n\t\tcatch\n\t\t{\n\t\t\t_audioCooldown = 0.2f;\n\t\t}\n\t}\n\n\tvoid EnsureSceneLighting()\n\t{\n\t\tif ( Scene == null ) return;\n\t\tforeach ( var l in Scene.GetAllComponents<DirectionalLight>() )\n\t\t\tif ( l != null ) return;\n\n\t\tvar lightGo = new GameObject( true, \"Auto Directional Light\" );\n\t\tlightGo.Flags = GameObjectFlags.NotNetworked;\n\t\tlightGo.WorldRotation = Rotation.LookAt( new Vector3( 0.4f, -0.25f, -1f ).Normal, Vector3.Up );\n\t\tvar d = lightGo.AddComponent<DirectionalLight>();\n\t\td.LightColor = new Color( 0.95f, 0.98f, 1f );\n\t\td.Shadows = true;\n\t}\n\n\t// -------------------- easing --------------------\n\n\tstatic float EaseInOutCubic( float t ) => t < 0.5f ? 4f * t * t * t : 1f - MathF.Pow( -2f * t + 2f, 3f ) / 2f;\n\tstatic float EaseOutCubic( float t ) => 1f - MathF.Pow( 1f - t, 3f );\n\tstatic float EaseOutBack( float t )\n\t{\n\t\tconst float c1 = 1.70158f;\n\t\tconst float c3 = c1 + 1f;\n\t\tfloat u = t - 1f;\n\t\treturn 1f + c3 * u * u * u + c1 * u * u;\n\t}\n}\n"
},
{
"Ident": "verarp.colour_break",
"Path": "ui/controls/switchcontrol.razor.scss",
"FileName": "switchcontrol.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "\r\n.switchcontrol\r\n{\r\n flex-direction: row;\r\n width: 100px;\r\n min-height: 24px;\r\n align-items: center;\r\n cursor: pointer;\r\n\r\n .switch-frame\r\n {\r\n flex-grow: 0;\r\n flex-shrink: 1;\r\n width: 48px;\r\n height: 16px;\r\n background-color: #fff1;\r\n margin: 0px 5px;\r\n align-items: center;\r\n border-radius: 100px;\r\n transition: all 0.4s linear;\r\n\r\n .switch-inner\r\n {\r\n position: relative;\r\n flex-grow: 0;\r\n flex-shrink: 1;\r\n background-color: #999;\r\n width: 25px;\r\n height: 25px;\r\n border-radius: 100px;\r\n left: 20%;\r\n transform: translateX( -50% );\r\n transition: all 0.3s ease-out;\r\n }\r\n }\r\n\r\n &.active\r\n {\r\n .switch-frame\r\n {\r\n background-color: #fffa;\r\n }\r\n\r\n .switch-inner\r\n {\r\n left: 80%;\r\n background-color: #fff;\r\n }\r\n }\r\n}\r\n"
},
{
"Ident": "verarp.colour_break",
"Path": "styles/form/_colorproperty.scss",
"FileName": "_colorproperty.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "\r\n\r\n.colorproperty\r\n{\r\n\talign-items: center;\r\n\tposition: relative;\r\n\r\n\t.colorsquare\r\n\t{\r\n\t\twidth: 20px;\r\n\t\theight: 20px;\r\n\t\tborder-radius: 4px;\r\n\t\tmargin-right: 8px;\r\n\t\tposition: absolute;\r\n\t\tleft: 7px;\r\n\t\tz-index: 1;\r\n\t\tcursor: pointer;\r\n\t}\r\n\r\n\t> .textentry:not( .a.b.c )\r\n\t{\r\n\t\tpadding-left: 36px;\r\n\t}\r\n}"
},
{
"Ident": "verarp.colour_break",
"Path": "ui/menupanel.razor.scss",
"FileName": "menupanel.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "\r\nmenupanel\r\n{\r\n position: absolute;\r\n z-index: 1000;\r\n pointer-events: all;\r\n font-size: 12px;\r\n flex-shrink: 0;\r\n\r\n .background\r\n {\r\n position: absolute;\r\n left: -5000px;\r\n right: -5000px;\r\n top: -5000px;\r\n bottom: -5000px;\r\n }\r\n\r\n > .inner\r\n {\r\n min-width: 200px;\r\n min-height: 20px;\r\n flex-direction: column;\r\n font-family: Poppins;\r\n font-weight: bold;\r\n border-radius: 10px;\r\n box-shadow: 5px 5px 30px #000e;\r\n background-color: #2a2a2a;\r\n flex-shrink: 0;\r\n\r\n .spacer\r\n {\r\n height: 1px;\r\n background-color: #0005;\r\n }\r\n\r\n .option\r\n {\r\n color: #fffa;\r\n padding: 0px 8px;\r\n cursor: pointer;\r\n flex-shrink: 0;\r\n height: 32px;\r\n\r\n &:first-child\r\n {\r\n border-top-left-radius: 10px;\r\n border-top-right-radius: 10px;\r\n }\r\n\r\n &:last-child\r\n {\r\n border-bottom-left-radius: 10px;\r\n border-bottom-right-radius: 10px;\r\n }\r\n\r\n .icon\r\n {\r\n padding: 8px;\r\n font-family: Material Icons;\r\n justify-content: center;\r\n align-items: center;\r\n flex-shrink: 0;\r\n }\r\n\r\n .text\r\n {\r\n padding: 8px;\r\n flex-shrink: 0;\r\n }\r\n\r\n &:hover\r\n {\r\n background-color: #3472e6;\r\n color: #f5f8fe;\r\n }\r\n }\r\n }\r\n}\r\n"
},
{
"Ident": "verarp.colour_break",
"Path": "ui/controls/color/colorcontrol.cs.scss",
"FileName": "colorcontrol.cs.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "ColorControl\r\n{\r\n\tgap: 0.5rem;\r\n\tflex-grow: 1;\r\n\tpointer-events: all;\r\n\tbackground-color: #000a;\r\n\tborder-radius: 4px;\r\n\tpadding: 2px;\r\n\theight: 32px;\r\n}\r\n\r\nColorControl TextEntry\r\n{\r\n\tflex-grow: 1;\r\n\tflex-shrink: 0;\r\n\tcolor: #aaa;\r\n\tfont-size: 1.2rem;\r\n\r\n\t&:hover, &:focus\r\n\t{\r\n\t\tcolor: #ddd;\r\n\r\n\t\t.icon\r\n\t\t{\r\n\t\t\tcolor: #3af;\r\n\t\t}\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\tcolor: white;\r\n\t}\r\n}\r\n\r\nColorControl > .colorswatch\r\n{\r\n\taspect-ratio: 1;\r\n\tborder-radius: 4px;\r\n\theight: 100%;\r\n\tflex-shrink: 0;\r\n\tcursor: pointer;\r\n\tborder: 2px solid #000;\r\n}"
},
{
"Ident": "verarp.colour_break",
"Path": "ui/components/packagefilters.razor.scss",
"FileName": "packagefilters.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 292528,
"IsPrivate": false,
"Code": "\r\n@import \"/styles/_theme.scss\";\r\n\r\n.packagefilter\r\n{\r\n\twidth: 100%;\r\n\tflex-shrink: 0;\r\n\r\n\t.left\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\tflex-shrink: 0;\r\n\t\tgap: 16px;\r\n\t}\r\n\r\n\t.right\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\tflex-shrink: 0;\r\n\t\tgap: 16px;\r\n\t}\r\n}\r\n\r\n.packagefilter textentry\r\n{\r\n\tflex-grow: 1;\r\n\tpadding: 8px 12px;\r\n\tpointer-events: all;\r\n\r\n\tLabel\r\n\t{\r\n\t\tmargin-left: 26px;\r\n\t}\r\n\r\n\ticonpanel\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tleft: 12px;\r\n\t\topacity: 0.3;\r\n\t\tfont-size: $text-large;\r\n\t}\r\n}\r\n\r\n\t.packagefilterfacet, .packagefilter textentry\r\n\t{\r\n\t\tbackground-color: rgba($default-950, 0.3);\r\n\t\tcolor: $default-text;\r\n\t\toutline: 1px solid $default-border;\r\n\t\ttransition: all 150ms $easing;\r\n\t\tborder-radius: $rounding-default;\r\n\t\tpadding: 4px 12px;\r\n\t\tgap: 16px;\r\n\t\tflex-shrink: 0;\r\n\t\tfont-size: 14px;\r\n\t\tcursor: pointer;\r\n\t\talign-items: center;\r\n\r\n\t\t.clear-button\r\n\t\t{\r\n\t\t\theight: 100%;\r\n\t\t\tmargin-right: -8px;\r\n\t\t\talign-items: center;\r\n\t\t\tpadding: 0px 4px;\r\n\r\n\t\t\t&:hover\r\n\t\t\t{\r\n\t\t\t\tcolor: red;\r\n\t\t\t\tborder-radius: 100%;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t&.is-active\r\n\t\t{\r\n\t\t\tbackground-color: $default-700;\r\n\t\t}\r\n\r\n\t\t&:not(.is-active)\r\n\t\t{\r\n\t\t\t&:hover\r\n\t\t\t{\r\n\t\t\t\toutline: 1px solid $default-50;\r\n\t\t\t}\r\n\t\t}\r\n\t}"
}
]
}