Implement native opening scenes, battle foundation and overworld movement
Checks / web (push) Canceled after 0s
Checks / web (push) Canceled after 0s
This commit is contained in:
1 parent
8b80d05ca2
commit
c08d250cf6
39 files changed
+10727
-1524
No files matched your search
@@ -0,0 +1,931 @@
|
||||
:root {
|
||||
font-family:
|
||||
Inter,
|
||||
ui-sans-serif,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
color: #183d36;
|
||||
background: #f4f4ec;
|
||||
font-synthesis: none;
|
||||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
font-synthesis: none;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.player-setup {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin: 12px 0;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.player-setup label {
|
||||
font-size: 10px;
|
||||
color: #647962;
|
||||
}
|
||||
.player-setup input,
|
||||
.player-setup select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
border: 1px solid #d5ddd1;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
padding: 8px;
|
||||
color: #234e40;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
.player-setup small {
|
||||
grid-column: 1 / -1;
|
||||
font-size: 9px;
|
||||
color: #7a8b7b;
|
||||
}
|
||||
.world-status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 9px;
|
||||
color: #73816f;
|
||||
gap: 8px;
|
||||
}
|
||||
.game-notice {
|
||||
color: #765b38;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
.game-notice:empty {
|
||||
display: none;
|
||||
}
|
||||
.native-dialogue {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
z-index: 1;
|
||||
padding: 12px;
|
||||
color: #17382b;
|
||||
background: #fffdf0;
|
||||
border: 2px solid #284c3c;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 3px 15px #102b2a55;
|
||||
}
|
||||
.native-dialogue p {
|
||||
font-size: clamp(11px, 2.8vw, 15px);
|
||||
line-height: 1.45;
|
||||
white-space: pre-line;
|
||||
margin: 0;
|
||||
max-height: 125px;
|
||||
overflow: auto;
|
||||
}
|
||||
.native-dialogue > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 9px;
|
||||
}
|
||||
.native-dialogue button {
|
||||
background: transparent;
|
||||
padding: 8px 0 0 12px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.pause-overlay {
|
||||
z-index: 2;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right)
|
||||
env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||
}
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
button,
|
||||
a,
|
||||
label {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.42;
|
||||
}
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
summary:focus-visible,
|
||||
canvas:focus-visible {
|
||||
outline: 3px solid #ca713a;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
button {
|
||||
color: inherit;
|
||||
}
|
||||
button:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
button,
|
||||
.file-button {
|
||||
min-height: 44px;
|
||||
}
|
||||
button {
|
||||
border: 0;
|
||||
}
|
||||
button:hover:not(:disabled) {
|
||||
filter: brightness(0.94);
|
||||
}
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
.site-header {
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
padding: 30px 54px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #dce1d6;
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 28px;
|
||||
letter-spacing: -1.5px;
|
||||
font-weight: 750;
|
||||
text-decoration: none;
|
||||
}
|
||||
.brand span {
|
||||
font-size: 9px;
|
||||
letter-spacing: 1.7px;
|
||||
font-weight: 600;
|
||||
margin-left: 13px;
|
||||
padding-left: 20px;
|
||||
border-left: 1px solid #c8d0c6;
|
||||
}
|
||||
.text-button {
|
||||
background: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
.text-button span {
|
||||
margin-left: 16px;
|
||||
}
|
||||
main {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 490px;
|
||||
gap: 85px;
|
||||
padding: 62px 40px 45px;
|
||||
}
|
||||
.intro {
|
||||
padding-top: 24px;
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 10px;
|
||||
letter-spacing: 2px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.small-star {
|
||||
font-size: 23px;
|
||||
color: #6d8d72;
|
||||
}
|
||||
h1 {
|
||||
font-size: clamp(38px, 4.8vw, 62px);
|
||||
line-height: 1.08;
|
||||
letter-spacing: -3px;
|
||||
font-weight: 500;
|
||||
margin: 26px 0;
|
||||
}
|
||||
h1 em {
|
||||
font-family: Georgia, serif;
|
||||
font-weight: 400;
|
||||
color: #759378;
|
||||
}
|
||||
.intro-copy {
|
||||
font-size: 15px;
|
||||
line-height: 1.85;
|
||||
color: #6a7b6e;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.load-card {
|
||||
background: #fbfcf7;
|
||||
border: 1px solid #dce2d6;
|
||||
border-radius: 15px;
|
||||
padding: 24px;
|
||||
max-width: 350px;
|
||||
box-shadow: 0 5px 18px #20352a03;
|
||||
}
|
||||
.card-heading {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.step-number {
|
||||
font-size: 10px;
|
||||
color: #738876;
|
||||
border: 1px solid #d9e1d5;
|
||||
border-radius: 50%;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.load-card h2 {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.load-card > p {
|
||||
color: #73816f;
|
||||
font-size: 12px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
.load-card strong {
|
||||
font-weight: 500;
|
||||
color: #2e5246;
|
||||
}
|
||||
.primary-button {
|
||||
background: #234e40;
|
||||
color: #fff;
|
||||
border: 1px solid #234e40;
|
||||
border-radius: 7px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 15px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
.file-input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
}
|
||||
.load-card:focus-within .file-button {
|
||||
outline: 3px solid #ca713a;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.load-card .privacy-note {
|
||||
font-size: 10px;
|
||||
margin: 13px 0 0;
|
||||
text-align: center;
|
||||
color: #7a8b7b;
|
||||
}
|
||||
.privacy-note span {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.project-note {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 30px;
|
||||
align-items: center;
|
||||
max-width: 350px;
|
||||
}
|
||||
.note-line {
|
||||
width: 3px;
|
||||
align-self: stretch;
|
||||
background: #d4ddce;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.project-note p {
|
||||
font-size: 11px;
|
||||
color: #576d5b;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.project-note p span {
|
||||
color: #8b9789;
|
||||
}
|
||||
.console {
|
||||
background: #133532;
|
||||
border-radius: 25px 25px 60px 25px;
|
||||
padding: 22px 25px 18px;
|
||||
box-shadow:
|
||||
0 23px 45px #1335321c,
|
||||
inset 0 1px 1px #ffffff28;
|
||||
color: #a7c6b8;
|
||||
border: 1px solid #244b42;
|
||||
}
|
||||
.console-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 9px;
|
||||
letter-spacing: 2px;
|
||||
padding: 0 3px 17px;
|
||||
}
|
||||
.power {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 8px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.power i {
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
border-radius: 50%;
|
||||
background: #6f887a;
|
||||
}
|
||||
.power i.on {
|
||||
background: #b2e6a5;
|
||||
box-shadow: 0 0 7px #b2e6a5;
|
||||
}
|
||||
.screen-bezel {
|
||||
padding: 15px 16px 9px;
|
||||
background: #0a2423;
|
||||
border-radius: 9px 9px 25px 9px;
|
||||
border: 1px solid #315249;
|
||||
}
|
||||
.screen {
|
||||
position: relative;
|
||||
aspect-ratio: 160/144;
|
||||
background: #dae6c7;
|
||||
overflow: hidden;
|
||||
border-radius: 3px;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
.screen-empty {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: #526c49;
|
||||
background: radial-gradient(ellipse at center, #e6ecd6, #ccdcbc);
|
||||
}
|
||||
.crystal-mark {
|
||||
font-size: 84px;
|
||||
line-height: 1.05;
|
||||
font-family: Georgia, serif;
|
||||
font-weight: 400;
|
||||
color: #64885d;
|
||||
text-shadow: 6px 0 #b6cbaa;
|
||||
margin-top: -8px;
|
||||
}
|
||||
.screen-empty h2 {
|
||||
font-family: Georgia, serif;
|
||||
font-size: 25px;
|
||||
line-height: 1.2;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.5px;
|
||||
margin: 16px 0 10px;
|
||||
}
|
||||
.screen-empty p {
|
||||
font-size: 10px;
|
||||
margin: 0;
|
||||
color: #718565;
|
||||
}
|
||||
.screen-dots {
|
||||
margin-top: 12px;
|
||||
font-size: 20px;
|
||||
letter-spacing: 5px;
|
||||
color: #8da17b;
|
||||
}
|
||||
.screen-caption {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 9px 0 0;
|
||||
letter-spacing: 2px;
|
||||
font-size: 7px;
|
||||
color: #6f8e7f;
|
||||
}
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
align-items: center;
|
||||
padding: 22px 8px 4px;
|
||||
gap: 15px;
|
||||
}
|
||||
.dpad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 44px);
|
||||
grid-template-rows: repeat(3, 44px);
|
||||
filter: drop-shadow(0 5px 1px #061918);
|
||||
}
|
||||
.game-key {
|
||||
min-width: 44px;
|
||||
flex-shrink: 0;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
.dpad .game-key,
|
||||
.dpad-center {
|
||||
background: #254d43;
|
||||
box-shadow: inset 0 1px #527363;
|
||||
color: #8fb09a;
|
||||
min-height: 44px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.dpad .game-key {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.dpad [data-game-button="up"] {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
.dpad [data-game-button="left"] {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
.dpad-center {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
box-shadow: none;
|
||||
}
|
||||
.dpad-center:after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 1px solid #365e50;
|
||||
border-radius: 50%;
|
||||
margin: 15px;
|
||||
}
|
||||
.dpad [data-game-button="right"] {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
.dpad [data-game-button="down"] {
|
||||
grid-column: 2;
|
||||
grid-row: 3;
|
||||
}
|
||||
.action-keys {
|
||||
display: flex;
|
||||
gap: 17px;
|
||||
justify-content: flex-end;
|
||||
transform: rotate(-20deg);
|
||||
}
|
||||
.action-keys button {
|
||||
width: 59px;
|
||||
height: 59px;
|
||||
border-radius: 50%;
|
||||
background: #d8e7c0;
|
||||
color: #35573e;
|
||||
font-size: 20px;
|
||||
font-weight: 650;
|
||||
border: 3px solid #b3caa0;
|
||||
box-shadow: 0 5px #081f1c;
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
.game-key:disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.game-key.pressed {
|
||||
filter: brightness(1.3);
|
||||
translate: 0 3px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.system-keys {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
grid-column: 1/3;
|
||||
justify-content: center;
|
||||
margin-top: -4px;
|
||||
}
|
||||
.system-keys button {
|
||||
background: none;
|
||||
position: relative;
|
||||
font-size: 8px;
|
||||
letter-spacing: 1px;
|
||||
padding-top: 22px;
|
||||
color: #86a696;
|
||||
}
|
||||
.system-keys button:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) rotate(-10deg);
|
||||
width: 33px;
|
||||
height: 9px;
|
||||
background: #416356;
|
||||
border: 1px solid #517463;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 3px #071f1c;
|
||||
}
|
||||
.speaker {
|
||||
display: none;
|
||||
}
|
||||
.console-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 7px;
|
||||
letter-spacing: 1.7px;
|
||||
color: #54776a;
|
||||
margin: 13px 5px 0;
|
||||
}
|
||||
.console-bottom > span:last-child {
|
||||
font-size: 18px;
|
||||
}
|
||||
.player-toolbar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
padding-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.player-toolbar button,
|
||||
.save-tools button {
|
||||
background: transparent;
|
||||
font-size: 11px;
|
||||
padding: 8px 11px;
|
||||
border: 1px solid #d5ddd1;
|
||||
border-radius: 7px;
|
||||
}
|
||||
.status {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
color: #647962;
|
||||
min-height: 17px;
|
||||
margin: 14px 0 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.save-status {
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
color: #8b9788;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.error {
|
||||
color: #a03e29 !important;
|
||||
}
|
||||
.save-tools {
|
||||
font-size: 11px;
|
||||
color: #71816c;
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.save-tools summary {
|
||||
cursor: pointer;
|
||||
min-height: 44px;
|
||||
padding: 12px;
|
||||
}
|
||||
.save-tools > div {
|
||||
padding: 12px;
|
||||
border: 1px solid #d5ddd1;
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
.save-tools button {
|
||||
margin: 0 5px 5px 0;
|
||||
}
|
||||
.save-tools p {
|
||||
font-size: 11px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.pause-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #0a2423b8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #e7efdb;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
.pause-overlay span {
|
||||
font-size: 10px;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
.pause-overlay button {
|
||||
background: #d8e7c0;
|
||||
color: #21483b;
|
||||
border-radius: 7px;
|
||||
padding: 10px 18px;
|
||||
font-size: 12px;
|
||||
}
|
||||
footer {
|
||||
max-width: 1172px;
|
||||
margin: auto;
|
||||
border-top: 1px solid #dce1d6;
|
||||
padding: 22px 0 28px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 15px;
|
||||
color: #8a9886;
|
||||
font-size: 10px;
|
||||
}
|
||||
dialog {
|
||||
border: 1px solid #d2ddca;
|
||||
background: #f4f4ec;
|
||||
color: #244c3e;
|
||||
border-radius: 18px;
|
||||
padding: 32px;
|
||||
max-width: 460px;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
dialog::backdrop {
|
||||
background: #0a242399;
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
dialog h2 {
|
||||
font-family: Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: 30px;
|
||||
}
|
||||
dialog p {
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
dialog dl {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
font-size: 13px;
|
||||
gap: 10px;
|
||||
}
|
||||
dialog dd {
|
||||
margin: 0;
|
||||
}
|
||||
.dialog-close {
|
||||
float: right;
|
||||
background: none;
|
||||
font-size: 25px;
|
||||
}
|
||||
@media (min-width: 1300px) {
|
||||
main {
|
||||
gap: 100px;
|
||||
padding-top: 75px;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
.intro {
|
||||
padding-top: 37px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1000px) {
|
||||
main {
|
||||
gap: 35px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(340px, 440px);
|
||||
padding: 40px 30px;
|
||||
}
|
||||
.site-header {
|
||||
padding: 22px 30px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 45px;
|
||||
}
|
||||
.console {
|
||||
padding: 20px;
|
||||
}
|
||||
.screen-empty h2 {
|
||||
font-size: 23px;
|
||||
}
|
||||
footer {
|
||||
margin: 0 30px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 740px) {
|
||||
.site-header {
|
||||
padding: 17px 22px;
|
||||
}
|
||||
.brand {
|
||||
font-size: 25px;
|
||||
}
|
||||
.brand img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.brand span {
|
||||
display: none;
|
||||
}
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
padding: 20px 18px 26px;
|
||||
max-width: 480px;
|
||||
}
|
||||
.intro {
|
||||
padding: 0;
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 8px;
|
||||
letter-spacing: 1.6px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
letter-spacing: -1.6px;
|
||||
margin: 15px 0 13px;
|
||||
}
|
||||
h1 br {
|
||||
display: block;
|
||||
}
|
||||
h1 em {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.intro-copy {
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
.intro-copy br {
|
||||
display: none;
|
||||
}
|
||||
.intro-copy br:after {
|
||||
content: " ";
|
||||
}
|
||||
#app.has-game .intro {
|
||||
display: none;
|
||||
}
|
||||
#app.has-game main {
|
||||
padding-top: 12px;
|
||||
}
|
||||
.load-card {
|
||||
max-width: none;
|
||||
padding: 15px 18px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 140px;
|
||||
column-gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.card-heading h2 {
|
||||
font-size: 12px;
|
||||
}
|
||||
.step-number {
|
||||
display: none;
|
||||
}
|
||||
.load-card > p {
|
||||
margin: 5px 0;
|
||||
font-size: 10px;
|
||||
grid-column: 1;
|
||||
}
|
||||
.load-card .primary-button {
|
||||
grid-column: 2;
|
||||
grid-row: 1/3;
|
||||
font-size: 11px;
|
||||
padding: 10px;
|
||||
gap: 5px;
|
||||
}
|
||||
.load-card .privacy-note {
|
||||
grid-column: 1/3;
|
||||
margin: 10px 0 0;
|
||||
font-size: 9px;
|
||||
}
|
||||
.project-note {
|
||||
display: none;
|
||||
}
|
||||
.console {
|
||||
border-radius: 22px 22px 50px 22px;
|
||||
padding: 20px;
|
||||
}
|
||||
.console-top {
|
||||
font-size: 8px;
|
||||
}
|
||||
.screen-bezel {
|
||||
padding: 12px 12px 9px;
|
||||
}
|
||||
.controls {
|
||||
padding: 22px 4px 0;
|
||||
}
|
||||
.action-keys button {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
}
|
||||
.action-keys {
|
||||
gap: 14px;
|
||||
}
|
||||
.player-toolbar {
|
||||
padding-top: 17px;
|
||||
}
|
||||
.player-toolbar button {
|
||||
font-size: 10px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.status {
|
||||
font-size: 10px;
|
||||
}
|
||||
footer {
|
||||
padding: 18px 0 25px;
|
||||
margin: 0 22px;
|
||||
flex-direction: column;
|
||||
font-size: 9px;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.load-card {
|
||||
grid-template-columns: 1fr 114px;
|
||||
padding: 13px;
|
||||
}
|
||||
.load-card .primary-button {
|
||||
font-size: 10px;
|
||||
}
|
||||
.console {
|
||||
padding: 15px;
|
||||
}
|
||||
.screen-bezel {
|
||||
padding: 9px;
|
||||
}
|
||||
.screen-empty h2 {
|
||||
font-size: 21px;
|
||||
}
|
||||
.screen-empty .crystal-mark {
|
||||
font-size: 62px;
|
||||
}
|
||||
.action-keys {
|
||||
gap: 8px;
|
||||
}
|
||||
.action-keys button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.dpad {
|
||||
grid-template-columns: repeat(3, 44px);
|
||||
grid-template-rows: repeat(3, 44px);
|
||||
}
|
||||
.controls {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
.system-keys {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
@media (max-height: 540px) and (orientation: landscape) {
|
||||
#app.has-game
|
||||
:is(
|
||||
.site-header,
|
||||
.intro,
|
||||
footer,
|
||||
.console-top,
|
||||
.console-bottom,
|
||||
.screen-caption,
|
||||
.save-tools,
|
||||
.save-status
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
#app.has-game main {
|
||||
display: block;
|
||||
max-width: 850px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
#app.has-game .console {
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
border-radius: 20px;
|
||||
min-height: 270px;
|
||||
}
|
||||
#app.has-game .screen-bezel {
|
||||
width: min(43%, 340px);
|
||||
margin: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
#app.has-game .controls {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
padding: 25px;
|
||||
}
|
||||
#app.has-game .controls > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
#app.has-game .system-keys {
|
||||
position: absolute;
|
||||
bottom: 18px;
|
||||
left: 30px;
|
||||
}
|
||||
#app.has-game .player-toolbar {
|
||||
padding-top: 8px;
|
||||
}
|
||||
#app.has-game .status {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
+55
-36
@@ -1,55 +1,74 @@
|
||||
const key = (name: string, label: string, className = "") =>
|
||||
`<button class="game-key ${className}" data-game-button="${name}" aria-label="${name === "a" || name === "b" ? name.toUpperCase() : name}" aria-pressed="false" disabled>${label}</button>`;
|
||||
const key = (name: string, label: string) =>
|
||||
`<button class="game-key" data-game-button="${name}" aria-label="${name === "a" || name === "b" ? name.toUpperCase() : name}" aria-pressed="false" disabled>${label}</button>`;
|
||||
|
||||
const tab = (id: string, label: string, selected = false) =>
|
||||
`<button id="tab-${id}" role="tab" aria-selected="${selected}" aria-controls="panel-${id}" tabindex="${selected ? "0" : "-1"}" data-tab="${id}">${selected ? '<span aria-hidden="true">▶</span>' : ""}${label}</button>`;
|
||||
|
||||
export function renderShell(root: HTMLElement) {
|
||||
root.innerHTML = `
|
||||
<header class="site-header">
|
||||
<a class="brand" href="./" aria-label="Crystal home"><img src="${import.meta.env.BASE_URL}icon.svg" alt="" width="36" height="36">crystal<span>NATIVE WEB PORT</span></a>
|
||||
<button id="help" class="text-button">How to play <span aria-hidden="true">↗</span></button>
|
||||
<a class="brand" href="./" aria-label="Crystal home">CRYSTAL</a>
|
||||
<div class="header-state"><span class="power"><i id="power-light"></i><span id="power-label">STANDBY</span></span><button id="help" class="text-button">Help</button></div>
|
||||
</header>
|
||||
<main>
|
||||
<section class="intro" aria-labelledby="page-title">
|
||||
<p class="eyebrow"><span class="small-star">✧</span> YOUR CARTRIDGE. A NEW HOME.</p>
|
||||
<h1 id="page-title">A little adventure.<br><em>Anywhere.</em></h1>
|
||||
<p class="intro-copy">Crystal, rebuilt for the web.<br> Start exploring New Bark Town in the first native milestone.</p>
|
||||
<div class="load-card">
|
||||
<div class="card-heading"><span class="step-number">01</span><h2>Bring your cartridge</h2></div>
|
||||
<p>Pokémon Crystal<br><strong>English US/Europe · Revision 1.1</strong></p>
|
||||
<div class="player-setup"><label>Name <input id="player-name" value="CHRIS" maxlength="7" pattern="[A-Za-z0-9 -]{1,7}" autocomplete="off"></label><label>Character <select id="player-avatar"><option value="chris">Chris</option><option value="kris">Kris</option></select></label><small>Used for your first session. Saved sessions keep their character.</small></div>
|
||||
<label class="primary-button file-button" id="rom-label" for="rom-file"><span aria-hidden="true">+</span> Choose ROM <span aria-hidden="true">→</span></label>
|
||||
<input id="rom-file" class="file-input" type="file" accept=".gbc,.gb,.rom,application/octet-stream" />
|
||||
<p class="privacy-note"><span aria-hidden="true">◇</span> Opened locally. Never uploaded.</p>
|
||||
</div>
|
||||
<div class="project-note"><span class="note-line"></span><p>An exploration build, not the full game yet.<br><span>Move, enter buildings, read signs and try interactions. Story progression, battles and audio are next.</span></p></div>
|
||||
</section>
|
||||
<section class="play-area" aria-label="Game player">
|
||||
<main class="app-shell">
|
||||
<nav class="tabs" role="tablist" aria-label="Crystal sections">
|
||||
${tab("game", "Game", true)}${tab("saves", "Savegames")}${tab("map", "Map")}${tab("pokedex", "Pokédex")}${tab("link", "Link")}${tab("friends", "Friends")}
|
||||
</nav>
|
||||
<section id="panel-game" class="tab-panel game-panel" role="tabpanel" aria-labelledby="tab-game" data-panel="game">
|
||||
<div class="console">
|
||||
<div class="console-top"><span>NATIVE / EXPLORATION</span><span class="power"><i id="power-light"></i><span id="power-label">STANDBY</span></span></div>
|
||||
<div class="screen-bezel">
|
||||
<div class="screen">
|
||||
<canvas id="game-screen" width="160" height="144" tabindex="0" aria-label="Game screen. Arrow keys move, X is A, Z is B, Enter is Start, Shift is Select."></canvas>
|
||||
<div id="native-dialogue" class="native-dialogue" aria-label="Dialogue" hidden><p id="dialogue-text" aria-live="polite"></p><div><span id="dialogue-page"></span><button id="dialogue-next">Continue <span aria-hidden="true">▸</span></button></div></div>
|
||||
<div id="screen-empty" class="screen-empty"><div class="crystal-mark" aria-hidden="true">◇</div><h2>Your next chapter<br>starts here.</h2><p>Load your Crystal ROM to begin.</p><span class="screen-dots" aria-hidden="true">· · ·</span></div>
|
||||
<div id="pause-overlay" class="pause-overlay" hidden><span>PAUSED</span><button id="screen-resume">Continue playing →</button></div>
|
||||
<div id="screen-empty" class="screen-empty">
|
||||
<form class="load-card" onsubmit="return false">
|
||||
<p class="menu-title">NEW SESSION</p><h1>Load your Crystal ROM</h1><p>English US/Europe · Revision 1.1</p>
|
||||
<div class="player-setup">
|
||||
<label>Name <input id="player-name" value="CHRIS" maxlength="7" pattern="[A-Za-z0-9 -]{1,7}" autocomplete="off"></label>
|
||||
<label>Character <select id="player-avatar"><option value="chris">Chris</option><option value="kris">Kris</option></select></label>
|
||||
</div>
|
||||
<label class="primary-button file-button" id="rom-label" for="rom-file"><span aria-hidden="true">▶</span> Choose ROM</label>
|
||||
<p class="privacy-note">Opened locally. Never uploaded.</p>
|
||||
</form>
|
||||
</div>
|
||||
<div id="battle" class="battle" hidden aria-label="Battle">
|
||||
<div class="battle-foe"><div class="battle-hud"><div><strong id="battle-foe-name"></strong><span id="battle-foe-level"></span></div><div class="hp-row"><span>HP</span><span class="hp-bar"><i id="battle-foe-hp"></i></span></div><span id="battle-foe-status" class="battle-status"></span></div><canvas id="battle-foe-pic" width="56" height="56" role="img"></canvas></div>
|
||||
<div id="battle-player" class="battle-player"><canvas id="battle-player-pic" width="48" height="48" role="img"></canvas><div class="battle-hud"><div><strong id="battle-player-name"></strong><span id="battle-player-level"></span></div><div class="hp-row"><span>HP</span><span class="hp-bar"><i id="battle-player-hp"></i></span></div><div class="battle-numbers"><span id="battle-player-status" class="battle-status"></span><span id="battle-player-hp-text"></span></div></div></div>
|
||||
<div class="battle-box"><p id="battle-text" aria-live="polite"></p><div id="battle-menu" class="battle-menu"></div></div>
|
||||
</div>
|
||||
<div id="pokepic" class="pokepic" hidden><canvas id="pokepic-canvas" width="56" height="56" role="img"></canvas><button id="pokepic-next" hidden>Continue <span aria-hidden="true">▶</span></button></div>
|
||||
<div id="native-dialogue" class="native-dialogue" aria-label="Dialogue" hidden><p id="dialogue-text" aria-live="polite"></p><div id="dialogue-choice" class="dialogue-choice" hidden></div><div><span id="dialogue-page"></span><button id="dialogue-next">Continue <span aria-hidden="true">▶</span></button></div></div>
|
||||
<div id="pause-overlay" class="pause-overlay" hidden><span>PAUSED</span><button id="screen-resume">Continue playing</button></div>
|
||||
<aside class="minimap-overlay" aria-label="Current area map"><canvas id="minimap" width="160" height="128"></canvas><p id="minimap-position" hidden>Map unavailable</p></aside>
|
||||
</div>
|
||||
<div class="screen-caption"><span id="location">YOUR WORLD, REBUILT</span><span>160 × 144</span></div>
|
||||
<div class="screen-caption"><span id="location">YOUR WORLD</span><span id="tiles-walked">0 tiles walked</span></div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="controls" aria-label="Game controls">
|
||||
<div class="dpad" aria-label="Directional controls">${key("up", "▲")}${key("left", "◀")}<span class="dpad-center" aria-hidden="true"></span>${key("right", "▶")}${key("down", "▼")}</div>
|
||||
<div class="action-keys">${key("b", "B")}${key("a", "A")}</div>
|
||||
<div class="system-keys">${key("select", "SELECT")}${key("start", "START")}</div>
|
||||
<div class="speaker" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
</div>
|
||||
<div class="console-bottom"><span>MADE FOR THE WAY YOU PLAY</span><span>✧</span></div>
|
||||
</div>
|
||||
<div class="player-toolbar" aria-label="Player actions"><button id="play" disabled>Play</button><button id="save" disabled>Save session</button><button id="backup" disabled>Export backup</button></div>
|
||||
<p class="world-status"><span id="tiles-walked">0 tiles walked</span><span>Exploration build · Audio pending</span></p>
|
||||
<p id="game-notice" role="status" class="game-notice"></p>
|
||||
<p id="status" class="status" role="status" aria-live="polite">Ready when you are. Choose your ROM to get started.</p>
|
||||
<p id="save-status" class="save-status">Session backups stay in this browser.</p>
|
||||
<details class="save-tools"><summary>Save files & compatibility</summary><div><button id="import-save" disabled>Import native backup</button><input id="save-file" class="file-input" type="file" accept=".json" /><p>Native saves keep your character, position and completed interactions. They are separate from emulator snapshots and cartridge .sav files, which cannot be imported yet.</p><p>Time-of-day colors follow your device clock. The tile counter measures in-game movement, not real-world steps. Export a backup before clearing browser data.</p></div></details>
|
||||
</section>
|
||||
<section id="panel-saves" class="tab-panel menu-panel" role="tabpanel" aria-labelledby="tab-saves" data-panel="saves" hidden>
|
||||
<header><p class="menu-title">SAVEGAMES</p><h2>Your local session</h2></header>
|
||||
<div class="save-actions"><button id="save" disabled>Save session</button><button id="backup" disabled>Export backup</button><button id="import-save" disabled>Import native backup</button><input id="save-file" class="file-input" type="file" accept=".json"></div>
|
||||
<div class="save-copy"><p>Native backups include your character, position and completed interactions. They are separate from cartridge .sav files.</p><p>Export before clearing browser data.</p></div>
|
||||
<div class="replace-rom"><h3>Cartridge</h3><p>Select a ROM again after reloading, or choose another compatible copy.</p><label class="menu-button file-button" for="rom-file"><span aria-hidden="true">▶</span> Choose or replace ROM</label></div>
|
||||
</section>
|
||||
<section id="panel-map" class="tab-panel menu-panel map-panel" role="tabpanel" aria-labelledby="tab-map" data-panel="map" hidden>
|
||||
<header><p class="menu-title">MAP</p><h2 id="map-location">No area loaded</h2><p id="map-position">Choose a ROM to see your position.</p></header>
|
||||
<div class="world-map-frame"><canvas id="world-map" width="160" height="128"></canvas><p id="map-empty">Your current area will appear here after loading a game. A wider Johto and Kanto atlas is planned.</p></div>
|
||||
</section>
|
||||
<section id="panel-pokedex" class="tab-panel menu-panel planned-panel" role="tabpanel" aria-labelledby="tab-pokedex" data-panel="pokedex" hidden><p class="menu-title">POKÉDEX</p><h2>Planned</h2><p>Track the Pokémon you have seen and caught. Available after encounters and catching arrive.</p></section>
|
||||
<section id="panel-link" class="tab-panel menu-panel planned-panel" role="tabpanel" aria-labelledby="tab-link" data-panel="link" hidden><p class="menu-title">LINK</p><h2>Planned</h2><p>Trade and battle with other players. Online Link is planned.</p></section>
|
||||
<section id="panel-friends" class="tab-panel menu-panel planned-panel" role="tabpanel" aria-labelledby="tab-friends" data-panel="friends" hidden><p class="menu-title">FRIENDS</p><h2>Planned</h2><p>Keep your friends together and send private Link invites. Friends are planned.</p></section>
|
||||
</main>
|
||||
<footer><span>A personal cartridge, a pocket-sized world.</span><span>Independent project · No game assets included</span></footer>
|
||||
<dialog id="help-dialog"><button id="close-help" class="dialog-close" aria-label="Close help">×</button><p class="eyebrow">NATIVE PORT · FIRST MILESTONE</p><h2>Make yourself at home.</h2><p>Choose your own English US/Europe revision 1.1 ROM, then press Play. Graphics, maps, sprites and dialogue are read locally from your ROM. Select it again after a reload to resume.</p><p>Explore your bedroom, downstairs, New Bark Town and its buildings. Use A to read signs, the radio and supported conversations. An unfinished interaction shows a message and leaves progress unchanged.</p><dl><dt>Move</dt><dd>Arrow keys</dd><dt>Interact / Continue</dt><dd>X / Z or A / B</dd></dl><p>Sessions save every 10 seconds and when paused. Download a backup before clearing browser data. Switching apps pauses play.</p><p>This is an exploration milestone. Opening scenes, story progression, battles, NPC movement, room decorations, audio and routes beyond town are not ported yet. Online Link and real-world steps come after the original game systems.</p></dialog>`;
|
||||
<div class="global-bar">
|
||||
<button id="play" class="play-button" disabled>Play</button>
|
||||
<p id="game-notice" role="status" class="game-notice"></p>
|
||||
<p id="status" class="status" role="status" aria-live="polite">Ready when you are. Choose your ROM to get started.</p>
|
||||
<p id="save-status" class="save-status">Session backups stay in this browser.</p>
|
||||
</div>
|
||||
<input id="rom-file" class="file-input" type="file" accept=".gbc,.gb,.rom,application/octet-stream">
|
||||
<dialog id="help-dialog"><button id="close-help" class="dialog-close" aria-label="Close help">×</button><p class="menu-title">HOW TO PLAY</p><h2>Explore New Bark Town</h2><p>Choose your own English US/Europe revision 1.1 ROM, then press Play. Graphics, maps, sprites and dialogue are read locally from your ROM.</p><dl><dt>Move</dt><dd>Arrow keys</dd><dt>Interact / Continue</dt><dd>X / Z or A / B</dd></dl><p>Sessions save every 10 seconds and when paused. Switching tabs or apps pauses play.</p><p>This milestone includes your home, New Bark Town and Professor Elm's lab, with the original scenes, choices and your first Pokémon. Battles, routes, menus and audio are still planned.</p><dl><dt>Choose</dt><dd>Up / Down, then X or A. Z or B answers No.</dd></dl><p>An optional walking companion may support egg progress after party, eggs and hatching are complete.</p></dialog>`;
|
||||
}
|
||||
+786
-778
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,49 @@
|
||||
const tabIds = ["game", "saves", "map", "pokedex", "link", "friends"] as const;
|
||||
type TabId = (typeof tabIds)[number];
|
||||
|
||||
export function bindTabs(root: HTMLElement, onLeaveGame: () => void) {
|
||||
const tabs = tabIds.map((id) =>
|
||||
root.querySelector<HTMLButtonElement>(`#tab-${id}`),
|
||||
);
|
||||
const panels = tabIds.map((id) =>
|
||||
root.querySelector<HTMLElement>(`#panel-${id}`),
|
||||
);
|
||||
if (tabs.some((item) => !item) || panels.some((item) => !item))
|
||||
throw new Error("Crystal tabs require every tab and panel.");
|
||||
|
||||
let current: TabId = "game";
|
||||
const select = (candidate: string) => {
|
||||
if (!tabIds.includes(candidate as TabId)) return;
|
||||
const next = candidate as TabId;
|
||||
if (current === "game" && next !== "game") onLeaveGame();
|
||||
current = next;
|
||||
tabIds.forEach((id, index) => {
|
||||
const selected = id === current;
|
||||
const button = tabs[index]!;
|
||||
button.setAttribute("aria-selected", String(selected));
|
||||
button.tabIndex = selected ? 0 : -1;
|
||||
button.innerHTML = `${selected ? '<span aria-hidden="true">▶</span>' : ""}${button.textContent?.replace(/^▶/, "").trim() ?? ""}`;
|
||||
panels[index]!.hidden = !selected;
|
||||
});
|
||||
};
|
||||
|
||||
tabs.forEach((tab, index) => {
|
||||
tab!.addEventListener("click", () => select(tabIds[index]));
|
||||
tab!.addEventListener("keydown", (event) => {
|
||||
let next = index;
|
||||
if (event.key === "ArrowRight" || event.key === "ArrowDown")
|
||||
next = (index + 1) % tabs.length;
|
||||
else if (event.key === "ArrowLeft" || event.key === "ArrowUp")
|
||||
next = (index - 1 + tabs.length) % tabs.length;
|
||||
else if (event.key === "Home") next = 0;
|
||||
else if (event.key === "End") next = tabs.length - 1;
|
||||
else return;
|
||||
event.preventDefault();
|
||||
tabs[next]!.focus();
|
||||
select(tabIds[next]);
|
||||
});
|
||||
});
|
||||
|
||||
select(current);
|
||||
return { select, active: () => current };
|
||||
}
|
||||
Reference in new issue
Block a user