ba-cafe

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 73d619d8473eaf1e585cfc36bf90239df5b0b014
parent 4e06384a1523fb8bbcc4f85c78ce78fab1df7548
Author: Sunny <122193933+Sunny-JP@users.noreply.github.com>
Date:   Sun,  7 Jun 2026 01:39:54 +0900

Merge pull request #63 from Sunny-JP/v5

V5
Diffstat:
Mpackage-lock.json | 407++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpackage.json | 3++-
Asrc/app/api/relationship/route.ts | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/app/gifts.png | 0
Msrc/app/globals.css | 852+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/app/page.tsx | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Asrc/components/BondDashboard.tsx | 614+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/BottomNavBar.tsx | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Asrc/components/NewFeatureDialog.tsx | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/Settings.tsx | 2+-
Msrc/components/TimerDashboard.tsx | 6+++---
Msrc/components/pages.tsx | 28+++++++++++++++++-----------
Asrc/lib/bondData.ts | 3267+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awiki2convert.py | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
14 files changed, 5461 insertions(+), 202 deletions(-)

diff --git a/package-lock.json b/package-lock.json @@ -21,7 +21,8 @@ "react": "19.2.3", "react-chartjs-2": "^5.3.1", "react-dom": "19.2.3", - "react-onesignal": "^3.4.4" + "react-onesignal": "^3.4.4", + "recharts": "^3.8.1" }, "devDependencies": { "@eslint/eslintrc": "^3.3.3", @@ -2557,6 +2558,42 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -2718,6 +2755,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@supabase/auth-js": { "version": "2.100.0", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.100.0.tgz", @@ -3180,6 +3229,69 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -3234,7 +3346,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3262,6 +3374,12 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -4610,6 +4728,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4698,9 +4825,130 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -4795,6 +5043,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5115,6 +5369,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5590,6 +5854,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -6167,6 +6437,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -6225,6 +6505,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -7886,7 +8175,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/react-onesignal": { @@ -7899,6 +8187,74 @@ "npm": ">=6" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8003,6 +8359,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -8804,6 +9166,12 @@ "integrity": "sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==", "license": "ISC" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -9225,6 +9593,37 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", diff --git a/package.json b/package.json @@ -23,7 +23,8 @@ "react": "19.2.3", "react-chartjs-2": "^5.3.1", "react-dom": "19.2.3", - "react-onesignal": "^3.4.4" + "react-onesignal": "^3.4.4", + "recharts": "^3.8.1" }, "devDependencies": { "@eslint/eslintrc": "^3.3.3", diff --git a/src/app/api/relationship/route.ts b/src/app/api/relationship/route.ts @@ -0,0 +1,128 @@ +import { NextResponse } from 'next/server'; +import { createClient } from '@supabase/supabase-js'; + +export const runtime = 'edge'; + +export async function OPTIONS() { + return new NextResponse(null, { + status: 204, + headers: { + 'Access-Control-Allow-Origin': 'https://rabbit1.cc', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); +} + +export async function GET(request: Request) { + try { + const authHeader = request.headers.get('Authorization'); + if (!authHeader) return NextResponse.json({ error: 'No token' }, { status: 401 }); + + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { global: { headers: { Authorization: authHeader } } } + ); + + const { data: { user }, error: authError } = await supabase.auth.getUser(); + if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + + const { data: relationships, error } = await supabase + .from('relationship') + .select('id, char_key, bond_level, recorded_at') + .eq('user_id', user.id) + .order('recorded_at', { ascending: false }); + + if (error) throw error; + + return NextResponse.json(relationships || []); + } catch (error: any) { + console.error("GET API Error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { char_key, bond_level, recorded_at } = body; + const authHeader = request.headers.get('Authorization'); + + if (!authHeader) return NextResponse.json({ error: 'No token' }, { status: 401 }); + + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { global: { headers: { Authorization: authHeader } } } + ); + + const { data: { user }, error: authError } = await supabase.auth.getUser(); + if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + + const { data: existingRecord, error: checkError } = await supabase + .from('relationship') + .select('id') + .eq('user_id', user.id) + .eq('char_key', char_key) + .eq('bond_level', bond_level) + .maybeSingle(); + + if (checkError) throw checkError; + + if (existingRecord) { + return NextResponse.json( + { error: `RANK ${bond_level} は既に登録されています。` }, + { status: 400 } + ); + } + + const { error } = await supabase + .from('relationship') + .insert({ + user_id: user.id, + char_key, + bond_level, + recorded_at + }); + + if (error) throw error; + + return NextResponse.json({ success: true }); + } catch (error: any) { + console.error("POST API Error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function DELETE(request: Request) { + try { + const body = await request.json(); + const { id } = body; + const authHeader = request.headers.get('Authorization'); + + if (!authHeader) return NextResponse.json({ error: 'No token' }, { status: 401 }); + + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { global: { headers: { Authorization: authHeader } } } + ); + + const { data: { user }, error: authError } = await supabase.auth.getUser(); + if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + + const { error } = await supabase + .from('relationship') + .delete() + .eq('id', id) + .eq('user_id', user.id); + + if (error) throw error; + + return NextResponse.json({ success: true }); + } catch (error: any) { + console.error("DELETE API Error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +}+ \ No newline at end of file diff --git a/src/app/gifts.png b/src/app/gifts.png Binary files differ. diff --git a/src/app/globals.css b/src/app/globals.css @@ -1,7 +1,10 @@ @import "tailwindcss"; +/* ========================================== + 1. VARIABLES & THEMES (:root / .dark) + ========================================== */ :root { - --background: #f0f8ff; /* わずかに青みのある白 */ + --background: #f0f8ff; --foreground: #1a2a3a; --card: #ffffff; --card-foreground: #1a2a3a; @@ -23,6 +26,9 @@ --tap-bg-6: hsl(var(--tap-h) var(--tap-s) 73%); --tap-bg-7: hsl(var(--tap-h) var(--tap-s) 66%); --tap-bg-8: hsl(var(--tap-h) var(--tap-s) 59%); + --system-color-scheme: light; + --svh: 100vh; + --bottom-nav-height: 56px; } .dark { @@ -40,48 +46,48 @@ --tap-h: 235; --tap-s: 40%; --tap-bg-0: #0f0e1a; - --tap-bg-8: hsl(var(--tap-h) var(--tap-s) 56%); - --tap-bg-7: hsl(var(--tap-h) var(--tap-s) 50%); - --tap-bg-6: hsl(var(--tap-h) var(--tap-s) 41%); - --tap-bg-5: hsl(var(--tap-h) var(--tap-s) 34%); - --tap-bg-4: hsl(var(--tap-h) var(--tap-s) 27%); - --tap-bg-3: hsl(var(--tap-h) var(--tap-s) 23%); - --tap-bg-2: hsl(var(--tap-h) var(--tap-s) 16%); --tap-bg-1: hsl(var(--tap-h) var(--tap-s) 12%); + --tap-bg-2: hsl(var(--tap-h) var(--tap-s) 16%); + --tap-bg-3: hsl(var(--tap-h) var(--tap-s) 23%); + --tap-bg-4: hsl(var(--tap-h) var(--tap-s) 27%); + --tap-bg-5: hsl(var(--tap-h) var(--tap-s) 34%); + --tap-bg-6: hsl(var(--tap-h) var(--tap-s) 41%); + --tap-bg-7: hsl(var(--tap-h) var(--tap-s) 50%); + --tap-bg-8: hsl(var(--tap-h) var(--tap-s) 56%); + --system-color-scheme: dark; } -html { - font-size: clamp(0.875rem, 0.5rem + 1vw, 1.125rem); -} - -:root { - --svh: 100vh; - --bottom-nav-height: 56px; -} @supports (height: 1svh) { :root { --svh: 100svh; } } -@media (min-width: 1000px) { - :root { - --bottom-nav-height: 0px; - } + +/* ========================================== + 2. GLOBAL RESET & BASE STYLES + ========================================== */ +*, +*::before, +*::after { + -ms-overflow-style: none !important; + scrollbar-width: none !important; +} + +*::-webkit-scrollbar { + display: none !important; +} + +html { + font-size: clamp(0.875rem, 0.5rem + 1vw, 1.125rem); } html, body, #__next { - height: var(--svh); - min-height: var(--svh); display: flex; flex-direction: column; -} - -header { - background-color: var(--background); - font-size: 1.35rem; - font-weight: 700; + height: var(--svh); + min-height: var(--svh); } body { @@ -92,10 +98,73 @@ body { padding-bottom: calc(var(--bottom-nav-height)); } +header { + background-color: var(--background); + font-size: 1.35rem; + font-weight: 700; +} + +/* ========================================== + 3. COMMON UI COMPONENTS + ========================================== */ +/* Forms & Inputs */ +input[type="range"] { + appearance: none; + -webkit-appearance: none; + background: transparent; + cursor: pointer; +} +input[type="range"]::-webkit-slider-thumb { + appearance: none; + -webkit-appearance: none; + height: 18px; + width: 18px; + border-radius: 50%; + background-color: white; + border: 4px solid var(--tap-bg-8); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); +} +input[type="range"]::-moz-range-thumb { + height: 18px; + width: 18px; + border-radius: 50%; + background-color: white; + border: 4px solid var(--tap-bg-8); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} + +/* Layout Containers */ +.dashboard-container { + padding: 4cqw 5cqw 2cqw 5cqw; +} +.cal-container { + padding: 2cqw 3cqw 2cqw 3cqw; +} +.bond-container { + padding: 5cqw 5cqw calc(var(--bottom-nav-height) - 4cqw) 5cqw; +} + +.view-panel { + display: none; + width: 100%; +} +.view-panel.active { + display: block; +} +.mobile-view { + display: block; + margin-top: 3rem; + padding: 0.5rem; +} +.desktop-view, +.desktop-hidden { + display: none; +} + +/* Buttons */ .btn-setting { flex: 1 1 0%; - padding-top: 8px; - padding-bottom: 8px; + padding: 8px 0; text-align: center; font-size: 1.2rem; line-height: 1.25; @@ -121,8 +190,7 @@ body { flex-direction: column; align-items: center; justify-content: center; - padding-top: 14px; - padding-bottom: 18px; + padding: 14px 0 18px 0; color: var(--nav-foreground); background-color: var(--background); transition: all 0.2s ease-in-out; @@ -136,12 +204,10 @@ body { stroke-width: 2.2; } .btn-nav:hover { - color: var(--primary); - opacity: 50%; + color: color-mix(in srgb, var(--primary) 65%, transparent); } .dark .btn-nav:hover { - color: var(--foreground); - opacity: 90%; + color: color-mix(in srgb, var(--foreground) 90%, transparent); } .btn-nav.active, .btn-nav:hover.active { @@ -152,14 +218,16 @@ body { .dark .btn-nav:hover.active { color: var(--foreground); } + .btn-nav-bar { + position: relative; + bottom: -8px; height: 2.5px; width: 0px; opacity: 0; transition: width 0.2s ease-out, opacity 0.2s ease-out; - bottom: -8px; } .btn-nav-bar.active { background-color: var(--primary); @@ -168,53 +236,41 @@ body { } .dark .btn-nav-bar.active { background-color: var(--foreground); - width: 50px; - opacity: 1; } -.dashboard-container { - container-type: inline-size; + +/* ========================================== + 4. APP MODULES (Timer, Calendar, Bond) + ========================================== */ +/* --- Timer Module --- */ +.timer-card { display: flex; flex-direction: column; - gap: 6cqw; - padding: 6cqw; -} -.cal-container { - container-type: inline-size; + justify-content: center; padding: 1cqw; -} - -.timer-card { + margin-bottom: 4cqw; color: var(--card-foreground); border-radius: 10px; - padding: 1cqw; - margin-bottom: 2cqw; backdrop-filter: blur(6px); - display: flex; - flex-direction: column; - justify-content: center; } .timer-card-title { - font-size: 4cqw; + font-size: 3cqw; font-weight: 700; color: var(--secondary-foreground); margin-bottom: 0.8cqw; + letter-spacing: 0.02em; } - .countdown-text-l { - font-size: 14cqw; - font-weight: 700; + font-size: 13cqw; letter-spacing: 0.05em; line-height: 1.1; margin-top: -1.6cqw; margin-bottom: 1.6cqw; } .countdown-text-s { - font-size: 8.5cqw; - font-weight: 700; + font-size: 7.5cqw; letter-spacing: 0.05em; line-height: 1.1; } - .slot-grid { display: grid; grid-template-columns: repeat(4, 1fr); @@ -227,21 +283,21 @@ body { flex-direction: column; align-items: center; justify-content: center; - border-style: none; min-height: 8cqw; - transition: all 0.2s; - line-height: 1.1; + border-style: none; font-weight: 700; + line-height: 1.1; clip-path: polygon(0 0, 88% 0, 100% 28%, 100% 100%, 0 100%); + transition: all 0.2s; } .slot-default { background-color: var(--card); color: var(--muted); } .slot-current { + position: relative; background-color: var(--card); color: var(--primary); - position: relative; text-decoration: none; overflow: hidden; } @@ -277,23 +333,23 @@ body { } .btn-timer { + position: relative; + display: flex; + align-items: center; + justify-content: center; width: 100%; - font-weight: 700; - padding: 3cqw 0; + height: auto; + padding: 2cqw 0; margin: 0; - font-size: 4.5cqw; - transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); + font-size: 3.5cqw; + font-weight: 700; background-color: var(--primary); color: var(--primary-foreground); - height: auto; - cursor: pointer; border: none; - transform: skewX(-10deg); border-radius: 8px; - position: relative; - display: flex; - align-items: center; - justify-content: center; + transform: skewX(-10deg); + cursor: pointer; + transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); } .btn-timer span, .btn-timer div { @@ -313,6 +369,7 @@ body { background-color: var(--muted); } +/* --- Calendar Module --- */ .cal-info { font-size: 6cqw; font-weight: 700; @@ -329,10 +386,10 @@ body { font-size: 52cqw; font-weight: 700; } + .cal-nav { flex: 1 1 0%; - padding-top: 14px; - padding-bottom: 14px; + padding: 14px 0; text-align: center; font-size: 4cqw; line-height: 1.25; @@ -365,6 +422,7 @@ body { .dark .cal-cell { text-shadow: none; } + .bg-tap-0 { background-color: var(--tap-bg-0); } @@ -393,80 +451,7 @@ body { background-color: var(--tap-bg-8); } -input[type="range"] { - -webkit-appearance: none; - appearance: none; - background: transparent; - cursor: pointer; -} -input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - height: 18px; - width: 18px; - background-color: white; - border: 4px solid var(--tap-bg-8); - border-radius: 50%; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); -} -input[type="range"]::-moz-range-thumb { - height: 18px; - width: 18px; - background-color: white; - border: 4px solid var(--tap-bg-8); - border-radius: 50%; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); -} - -.mobile-view { - display: block; - margin-top: 3rem; - padding: 0.5rem; -} -.desktop-view { - display: none; -} -.desktop-hidden { - display: flex; -} - -@media (min-width: 1000px) { - html { - font-size: 0.875rem; - } - .mobile-view { - display: none; - } - .desktop-view { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1.5rem; - margin-top: 2rem; - padding: 1.5rem; - } - .desktop-hidden { - display: none; - } - .dashboard-container { - gap: 2cqw; - padding: 3cqw; - } - - .timer-card-title { - font-size: 3cqw; - } - .countdown-text-l { - font-size: 11cqw; - } - .countdown-text-s { - font-size: 7.5cqw; - } - .btn-timer { - padding: 2cqw 0; - font-size: 3.5cqw; - } -} - +/* --- Glow Ring Effects --- */ .ba-ring-container { overflow: visible; } @@ -495,3 +480,566 @@ input[type="range"]::-moz-range-thumb { opacity: 0; } } + +/* --- Bond Dashboard --- */ +.bond-split-layout { + display: flex; + flex-direction: column; + gap: 5cqw; + width: 100%; +} +.bond-left-panel { + display: flex; + flex-direction: column; +} +.char-selector { + position: relative; + margin-bottom: 2cqw; +} +.char-selector-trigger { + display: flex; + flex-direction: column; + align-items: flex-start; + background: transparent; + border: none; + padding: 0; + cursor: pointer; +} +.char-selector-label, +.input-label { + font-size: 3cqw; + letter-spacing: 0.02em; + margin-bottom: 0.8cqw; + color: var(--secondary-foreground); +} +.char-selector-name { + display: flex; + align-items: baseline; + gap: 2cqw; + margin-top: 0.4cqw; + margin-bottom: 8px; + font-size: 7cqw; + font-weight: 900; + letter-spacing: -0.025em; + line-height: 1; +} +.char-selector-arrow { + font-size: 3.5cqw; + color: var(--secondary-foreground); + transition: transform 0.3s ease; +} +.char-selector-arrow.open { + transform: rotate(180deg); +} +.char-dropdown { + position: absolute; + top: 100%; + left: 0; + margin-top: 0.5cqw; + width: 45cqw; + max-width: 16rem; + max-height: 50cqw; + overflow-y: auto; + z-index: 50; + background-color: var(--card); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 2cqw; +} +.char-dropdown-container { + padding: 1.5cqw; +} +.char-dropdown-item { + width: 100%; + text-align: left; + padding: 1.5cqw 3cqw; + border: none; + border-radius: 1.5cqw; + background: transparent; + color: var(--foreground); + font-weight: 700; + font-size: 2.5cqw; + cursor: pointer; +} +.char-dropdown-item.active { + background-color: var(--primary); + color: var(--primary-foreground); +} +.record-input-area { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 4cqw; + border-bottom: 1px solid rgba(var(--muted), 0.2); + padding-bottom: 2cqw; +} +.input-group-container { + display: flex; + align-items: baseline; + gap: 4cqw; + flex: 1; +} +.input-field-wrapper { + display: flex; + flex-direction: column; +} +.input-content { + display: flex; + width: 15.5cqw; +} +.rank-prefix { + font-size: 3.5cqw; + font-weight: 900; + line-height: 1; + margin-top: 2.2cqw; +} +.rank-value { + display: inline-block; + width: 9cqw; + margin-left: 0.5cqw; + font-size: 6cqw; + font-weight: 900; + line-height: 1; +} +.spin-buttons-box { + display: inline-block; + vertical-align: baseline; + margin-left: 1.5cqw; +} +.spin-btn { + display: flex; + align-items: center; + justify-content: center; + width: 4cqw; + height: 3.2cqw; + background-color: rgba(var(--muted), 0.3); + border: none; + font-size: 2cqw; + color: var(--foreground); + cursor: pointer; +} +.spin-btn.up { + border-radius: 0.8cqw 0.8cqw 0 0; + margin-bottom: 2px; +} +.spin-btn.down { + border-radius: 0 0 0.8cqw 0.8cqw; +} +.date-input-display { + width: 42cqw; + margin-top: -0.5cqw; + font-size: 6cqw; + font-weight: 700; + line-height: 1; + letter-spacing: 0.03em; + background: transparent; + border: none; + outline: none; + padding: 0; + color-scheme: var(--system-color-scheme); +} +.btn-bond-record { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 18cqw; + height: 9cqw; + flex-shrink: 0; + font-weight: 700; + background-color: var(--primary); + color: var(--primary-foreground); + border: none; + border-radius: 1.5cqw; + transform: skewX(-10deg); + cursor: pointer; + transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); +} +.btn-bond-record:hover:not(:disabled) { + filter: brightness(1.1); + transform: skewX(-10deg) translateX(5px); +} +.btn-bond-record:disabled { + background-color: var(--muted); + color: var(--muted-foreground); + cursor: not-allowed; +} +.btn-bond-record span { + font-size: 2cqw; + font-weight: 900; + letter-spacing: 0.15em; + transform: skewX(10deg); + margin-bottom: -0.5cqw; +} +.btn-bond-record div { + font-size: 3cqw; + line-height: 1; + transform: skewX(10deg); +} +.btn-bond-record-dialog { + width: 100%; + margin-top: 0.5rem; + padding: 0.75rem 0; + border-radius: 0.75rem; + font-weight: 700; + background-color: var(--primary); + color: var(--primary-foreground); + border: none; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); +} +.btn-bond-record-dialog:hover:not(:disabled) { + filter: brightness(1.1); + transform: translateX(5px); +} +.btn-bond-record-dialog:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.section-wrapper { + margin-top: 4cqw; +} +.section-toggle-btn { + display: flex; + align-items: center; + gap: 1.5cqw; + margin-bottom: 1cqw; + font-size: 3cqw; + letter-spacing: 0.02em; + color: var(--secondary-foreground); + background: transparent; + border: none; + cursor: pointer; +} +.graph-container { + height: 55cqw; + min-height: 12rem; + width: 100%; + margin-top: 1.5cqw; +} +.graph-container * { + outline: none !important; +} +.graph-container .recharts-tooltip-item { + color: var(--primary) !important; +} +.dark .graph-container .recharts-tooltip-item { + color: var(--secondary-foreground) !important; +} + +.gift-title { + font-size: 3cqw; + letter-spacing: 0.02em; + margin-bottom: 2cqw; + color: var(--secondary-foreground); +} +.gift-scroll-container { + display: flex; + flex-wrap: nowrap; + gap: 1.5cqw; + width: 100%; + overflow-x: auto; + padding-top: 0.5cqw; + padding-bottom: 2cqw; +} +.gift-item { + display: flex; + flex-direction: column; + align-items: center; + flex-shrink: 0; + width: 50px; + height: 70px; +} +.gift-image-box { + position: relative; + width: 100%; + aspect-ratio: 1 / 1; + margin-bottom: 1cqw; +} +.gift-sprite { + position: absolute; + top: 50%; + left: 50%; + width: 142px; + height: 112px; + background-image: url("gifts.png"); + background-repeat: no-repeat; + background-position: calc(var(--col) * -142px) calc(var(--row) * -112px); + transform: translate(-50%, -50%) scale(0.48); + transform-origin: center center; + image-rendering: -webkit-optimize-contrast; +} +.gift-rank-dot { + position: absolute; + top: 0; + left: 0; + width: 2cqw; + height: 2cqw; + border-radius: 9999px; + border: 1px solid var(--background); + z-index: 10; +} +.gift-rank-dot.s { + background-color: #ef4444; +} +.gift-rank-dot.a { + background-color: #fb923c; +} +.gift-rank-dot.b { + background-color: #3b82f6; +} + +.gift-category-text { + width: 100%; + font-size: 2cqw; + min-height: 2.5cqw; + text-align: center; + font-weight: 500; + line-height: 1; + overflow-x: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +/* ========================================== + History Log Compact Grid Styles (5行×2列) + ========================================== */ +.history-grid-container { + display: grid; + grid-template-rows: repeat(5, minmax(0, 1fr)); + grid-auto-flow: column; + gap: 0.125rem 1.5rem; + margin-bottom: 2cqw; +} +.history-item { + display: flex; + align-items: center; + padding: 0.15rem 0.5rem; + border-bottom: 1px solid rgba(var(--muted), 0.05); + min-height: 1.75rem; +} +.history-log-row { + font-size: 0.875rem; + letter-spacing: 0.02em; + display: flex; + align-items: center; +} +.history-rank-num { + display: inline-block; + width: 1.5rem; + text-align: right; + font-weight: 700; + color: var(--foreground); +} +.history-divider { + margin: 0 0.5rem; + opacity: 0.25; + color: var(--foreground); +} +.history-date-text { + color: var(--secondary-foreground); +} +.history-empty-text { + color: var(--muted-foreground); + opacity: 0.4; +} +.edit-icon-btn { + display: flex; + align-items: center; + justify-content: center; + color: var(--secondary-foreground); + background: transparent; + border: none; + padding: 0.2rem; + margin-left: 0.3rem; + border-radius: 0.25rem; + cursor: pointer; + transition: all 0.2s ease; +} +.edit-icon-btn:hover { + color: var(--primary); + background-color: rgba(var(--muted), 0.1); +} +.edit-btn-placeholder { + display: inline-block; + width: 1.5rem; + height: 1px; + opacity: 0; + pointer-events: none; +} + +/* ========================================== + 5. MEDIA QUERIES (Desktop Optimization) + ========================================== */ +@media (min-width: 1000px) { + html { + font-size: 0.875rem; + } + + :root { + --bottom-nav-height: 56px; + } + + .dashboard-container { + container-type: inline-size; + display: flex; + flex-direction: column; + gap: 2cqw; + padding: 2cqw 2cqw 2cqw 5cqw; + } + .cal-container { + container-type: inline-size; + padding: 2cqw 5cqw 2cqw 2cqw; + } + .bond-container { + container-type: inline-size; + display: flex; + flex-direction: column; + padding: 2cqw 3cqw 2cqw 3cqw; + } + .bond-split-layout { + flex-direction: row; + align-items: stretch; + gap: 4rem; + } + .bond-left-panel { + flex: 1.2; + display: flex; + flex-direction: column; + } + .bond-right-panel { + flex: 1.8; + display: flex; + flex-direction: column; + border-left: 1px solid rgba(var(--muted), 0.15); + } + .char-selector { + margin-bottom: 1cqw; + } + .char-selector-name { + font-size: 3.5cqw; + gap: 0.5cqw; + } + .char-selector-label, + .input-label { + font-size: 1.5cqw; + } + .char-selector-arrow { + font-size: 1.3cqw; + } + .char-dropdown { + width: 16rem; + max-height: 20rem; + border-radius: 1rem; + } + .char-dropdown-container { + padding: 0.5cqw; + } + .char-dropdown-item { + font-size: 14px; + padding: 6px 12px; + } + .record-input-area { + padding-bottom: 1.5rem; + } + .input-content { + display: flex; + width: 6cqw; + } + .rank-prefix { + font-size: 1.3cqw; + margin-top: 1cqw; + } + .rank-value { + display: inline-block; + width: 5cqw; + margin-left: 0.5cqw; + font-size: 2.5cqw; + font-weight: 900; + line-height: 1; + } + .spin-buttons-box { + display: inline-block; + vertical-align: baseline; + margin-left: 0.5cqw; + margin-top: 0.2cqw; + scale: 1.3; + } + .spin-btn { + width: 1cqw; + height: 1cqw; + font-size: 0.8cqw; + } + .date-input-display { + font-size: 2.5cqw; + width: 17.5cqw; + margin-top: -0.25cqw; + } + .btn-bond-record { + width: 7cqw; + height: 4cqw; + border-radius: 0.75rem; + margin-left: 11cqw; + } + .btn-bond-record span { + font-size: 1cqw; + margin-bottom: -0.1cqw; + } + .btn-bond-record div { + font-size: 1cqw; + } + .section-wrapper { + margin-top: 1cqw; + } + .section-toggle-btn, + .gift-title { + font-size: 1.5cqw; + margin-bottom: 0.5cqw; + } + .gift-scroll-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(4rem, 1fr)); + gap: 0.5cqw 0; + overflow-x: visible; + } + .gift-image-box { + border-radius: 1rem; + margin-bottom: 0.5rem; + } + .gift-rank-dot { + width: 0.7cqw; + height: 0.7cqw; + } + .gift-category-text { + font-size: 0.7cqw; + } + .history-grid-container { + gap: 0.1cqw 2cqw; + } + .history-item { + padding: 0.1cqw 0.4cqw; + min-height: 1.5cqw; + } + .history-log-row { + font-size: 0.9cqw; + } + .history-rank-num { + width: 1.5cqw; + } + .history-divider { + margin: 0 0.4cqw; + } + .edit-icon-btn { + padding: 0.1cqw; + } + .edit-btn-placeholder { + width: 1.2cqw; + } + .graph-container { + flex: 1; + width: 100%; + height: 100% !important; + min-height: 25rem; + margin-top: 0.5cqw; + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx @@ -7,12 +7,14 @@ import Header from "@/components/Header"; import TimerDashboard from "@/components/TimerDashboard"; import BottomNavBar from "@/components/BottomNavBar"; import HistoryCalendar from "@/components/HistoryCalendar"; +import BondDashboard from "@/components/BondDashboard"; import Settings from "@/components/Settings"; import SidePanel from "@/components/SidePanel"; +import NewFeatureDialog from "@/components/NewFeatureDialog"; import { CALENDAR_LIMITS } from "@/lib/timeUtils"; import { OVERLAY_CONTENTS } from "@/components/pages"; -type Tab = 'timer' | 'history'; +type Tab = 'timer' | 'history' | 'bond'; const Overlay = ({ contentKey, onClose }: { contentKey: string; onClose: () => void }) => { const content = OVERLAY_CONTENTS[contentKey]; @@ -45,13 +47,30 @@ const Overlay = ({ contentKey, onClose }: { contentKey: string; onClose: () => v }; export default function Home() { - const [activeTab, setActiveTab] = useState<Tab>('timer'); + const [activeTab, setActiveTab] = useState<Tab>(() => { + if (typeof window !== 'undefined') { + const savedTab = localStorage.getItem('last_active_tab') as Tab | null; + const savedTimeStr = localStorage.getItem('last_active_tab_time'); + if (savedTab === 'timer' || savedTab === 'history' || savedTab === 'bond') { + if (savedTimeStr) { + const savedTime = parseInt(savedTimeStr, 10); + const now = Date.now(); + const TEN_MINUTES = 10 * 60 * 1000; + if (now - savedTime < TEN_MINUTES) { + return savedTab; + } + } + } + } + return 'timer'; + }); const [session, setSession] = useState<any>(null); const [timerHistory, setTimerHistory] = useState<number[]>([]); const [calendarHistory, setCalendarHistory] = useState<number[]>([]); const [calendarDate, setCalendarDate] = useState(() => new Date()); const [ticket1Time, setTicket1Time] = useState<Date | null>(null); const [ticket2Time, setTicket2Time] = useState<Date | null>(null); + const [relationshipHistory, setRelationshipHistory] = useState<any[]>([]); const [isSyncing, setIsSyncing] = useState(false); const [isDataLoaded, setIsDataLoaded] = useState(false); const [isAuthChecking, setIsAuthChecking] = useState(true); @@ -60,6 +79,49 @@ export default function Home() { const isInitialFetched = useRef(false); + useEffect(() => { + if (typeof window !== 'undefined' && activeTab) { + localStorage.setItem('last_active_tab', activeTab); + localStorage.setItem('last_active_tab_time', Date.now().toString()); + } + }, [activeTab]); + + const fetchRelationshipHistory = useCallback(async () => { + const { data: { session: s } } = await supabase.auth.getSession(); + if (!s) return; + try { + const res = await fetch('/api/relationship', { + headers: { 'Authorization': `Bearer ${s.access_token}` } + }); + const data = await res.json(); + setRelationshipHistory(Array.isArray(data) ? data : []); + } catch (e) { console.error(e); } + }, []); + + const handleSaveRelationship = async (level: number, date: string, charKey: string) => { + if (!session || isSyncing) return; + setIsSyncing(true); + try { + const res = await fetch('/api/relationship', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${session.access_token}` }, + body: JSON.stringify({ char_key: charKey, bond_level: level, recorded_at: date }) + }); + + if (!res.ok) { + const errorData = await res.json(); + alert(errorData.error || "登録に失敗しました。"); + return; + } + + await fetchRelationshipHistory(); + } catch (e) { + console.error(e); + } finally { + setIsSyncing(false); + } + }; + const fetchMonthlyData = useCallback(async (year: number, month: number) => { const { data: { session: s } } = await supabase.auth.getSession(); if (!s?.user?.id) return []; @@ -102,7 +164,8 @@ export default function Home() { setCalendarDate(new Date(year, month - 1, 1)); const [profileRes, monthlyData] = await Promise.all([ supabase.from('profiles').select('ticket1_time, ticket2_time').eq('id', s.user.id).single(), - fetchMonthlyData(year, month) + fetchMonthlyData(year, month), + fetchRelationshipHistory() ]); if (profileRes.data) { if (profileRes.data.ticket1_time) setTicket1Time(new Date(profileRes.data.ticket1_time)); @@ -112,7 +175,7 @@ export default function Home() { setTimerHistory(monthlyData); setIsDataLoaded(true); } finally { setIsAuthChecking(false); } - }, [fetchMonthlyData]); + }, [fetchMonthlyData, fetchRelationshipHistory]); useEffect(() => { supabase.auth.getSession().then(({ data: { session: s } }) => { @@ -211,11 +274,14 @@ export default function Home() { <div className="bg-background h-screen flex flex-col"> <OneSignalInit /> <Header isLoggedIn={!!session} onMenuClick={() => setIsSidePanelOpen(true)} /> + <NewFeatureDialog /> {overlayKey && <Overlay contentKey={overlayKey} onClose={() => setOverlayKey(null)} />} - <main className="flex-1 flex flex-col pt-16 pb-16 min-[1000px]:pb-0"> - <div className="min-[1000px]:hidden flex-1"> + <main className="flex-1 flex flex-col pt-16 pb-16"> + + {/* スマホ表示 (幅1000px未満) */} + <div className="min-[1000px]:hidden flex-1 overflow-y-auto"> {activeTab === 'timer' && ( <TimerDashboard tapHistory={timerHistory} lastTapTime={lastTapTime} ticket1Time={ticket1Time} ticket2Time={ticket2Time} onTap={handleTap} onInvite={handleInvite} isSyncing={isSyncing} isDataLoaded={isDataLoaded} /> )} @@ -224,14 +290,33 @@ export default function Home() { <HistoryCalendar tapHistory={calendarHistory} currentDate={calendarDate} onMonthChange={handleMonthChange} /> </div> )} + {activeTab === 'bond' && ( + <BondDashboard bondHistory={relationshipHistory} onSave={handleSaveRelationship} isSyncing={isSyncing} /> + )} </div> - <div className="hidden min-[1000px]:flex flex-1 items-center justify-center p-6 h-[calc(100vh-64px)] overflow-hidden"> - <div className="grid grid-cols-2 gap-6 w-full max-w-[160svh] items-stretch mx-auto"> - <TimerDashboard tapHistory={timerHistory} lastTapTime={lastTapTime} ticket1Time={ticket1Time} ticket2Time={ticket2Time} onTap={handleTap} onInvite={handleInvite} isSyncing={isSyncing} isDataLoaded={isDataLoaded} /> - <HistoryCalendar tapHistory={calendarHistory} currentDate={calendarDate} onMonthChange={handleMonthChange} /> + + {/* PC表示 (幅1000px以上) */} + <div className="hidden min-[1000px]:flex flex-1 justify-center px-4 py-4 h-[calc(100vh-120px)] overflow-hidden"> + <div className="w-full h-full mx-auto flex items-stretch"> + + {(activeTab === 'timer' || activeTab === 'history') && ( + <div className="grid grid-cols-2 gap-8 w-full h-full items-stretch animate-in fade-in duration-200"> + <TimerDashboard tapHistory={timerHistory} lastTapTime={lastTapTime} ticket1Time={ticket1Time} ticket2Time={ticket2Time} onTap={handleTap} onInvite={handleInvite} isSyncing={isSyncing} isDataLoaded={isDataLoaded} /> + <HistoryCalendar tapHistory={calendarHistory} currentDate={calendarDate} onMonthChange={handleMonthChange} /> + </div> + )} + + {activeTab === 'bond' && ( + <div className="w-full h-full overflow-y-auto animate-in fade-in"> + <BondDashboard bondHistory={relationshipHistory} onSave={handleSaveRelationship} isSyncing={isSyncing} /> + </div> + )} + </div> </div> + <BottomNavBar activeTab={activeTab} setActiveTab={setActiveTab} /> + <SidePanel isOpen={isSidePanelOpen} onClose={() => setIsSidePanelOpen(false)}> <Settings onOpenContent={(key) => { setOverlayKey(key); setIsSidePanelOpen(false); }} /> </SidePanel> diff --git a/src/components/BondDashboard.tsx b/src/components/BondDashboard.tsx @@ -0,0 +1,613 @@ +"use client"; + +import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react'; +import { + XAxis, YAxis, CartesianGrid, + ResponsiveContainer, AreaChart, Area, Tooltip, Line +} from 'recharts'; +import { format } from 'date-fns'; +import { + CHARACTER_LIST, GIFT_LIST, + SPRITE_CONFIG, CharacterId, + BOND_EXP_TABLE +} from '@/lib/bondData'; + +interface BondRecord { + id: string; + char_key: string; + bond_level: number; + recorded_at: string; +} + +const MIN_CHART_DIMENSION = 32; + +const getLevelFromExp = (exp: number): number => { + let currentLevel = 1; + for (let lv = 1; lv <= 100; lv++) { + if (BOND_EXP_TABLE[lv] !== undefined && exp >= BOND_EXP_TABLE[lv]) { + currentLevel = lv; + } else { + break; + } + } + return currentLevel; +}; + +const TrashIcon = () => ( + <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> + <path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> + </svg> +); + +export default function BondDashboard({ + bondHistory, + onSave, + isSyncing +}: { + bondHistory: BondRecord[], + onSave: (level: number, date: string, charKey: string) => Promise<void>, + isSyncing?: boolean +}) { + const [selectedCharId, setSelectedCharId] = useState<CharacterId>(CHARACTER_LIST[0].id); + const [isMounted, setIsMounted] = useState(false); + + const containerRef = useRef<HTMLDivElement | null>(null); + const [isChartReady, setIsChartReady] = useState(false); + + useEffect(() => { + if (typeof window !== 'undefined') { + const savedCharId = localStorage.getItem('last_selected_char_id') as CharacterId | null; + if (savedCharId && CHARACTER_LIST.some(c => c.id === savedCharId)) { + setSelectedCharId(savedCharId); + } + setIsMounted(true); + } + }, []); + + useLayoutEffect(() => { + const el = containerRef.current; + if (!el) return; + + const update = (rect = el.getBoundingClientRect()) => { + if (rect.width >= MIN_CHART_DIMENSION && rect.height >= MIN_CHART_DIMENSION) { + setIsChartReady(true); + } + }; + + update(); + if (typeof ResizeObserver === "undefined") return; + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.target === el) update(entry.contentRect); + } + }); + observer.observe(el); + + return () => observer.disconnect(); + }, []); + + const [inputLevel, setInputLevel] = useState(1); + const [inputDate, setInputDate] = useState(format(new Date(), 'yyyy-MM-dd')); + + const [isSelectOpen, setIsSelectOpen] = useState(false); + const [isDesktop, setIsDesktop] = useState(false); + + const dropdownRef = useRef<HTMLDivElement>(null); + + const selectedChar = useMemo(() => + CHARACTER_LIST.find(c => c.id === selectedCharId) || CHARACTER_LIST[0], + [selectedCharId]); + + const sortedGifts = useMemo(() => { + const ratingOrder: Record<string, number> = { + 'ss': 1, 'sa': 2, 'ns': 3, 'na': 4, 'nb': 5 + }; + + return GIFT_LIST + .filter(gift => selectedChar.giftRatings[gift.name]) + .sort((a, b) => { + const ratingA = selectedChar.giftRatings[a.name]?.toLowerCase() || ''; + const ratingB = selectedChar.giftRatings[b.name]?.toLowerCase() || ''; + const orderA = ratingOrder[ratingA] || 99; + const orderB = ratingOrder[ratingB] || 99; + return orderA - orderB; + }); + }, [selectedChar]); + + const filteredHistory = useMemo(() => { + return bondHistory + .filter(h => h.char_key === selectedCharId) + .sort((a, b) => b.recorded_at.localeCompare(a.recorded_at)); + }, [bondHistory, selectedCharId]); + + const displayHistoryRows = useMemo(() => { + const rows = []; + for (let i = 0; i < 10; i++) { + rows.push(filteredHistory[i] || null); + } + return rows; + }, [filteredHistory]); + + const chartDataCombined = useMemo(() => { + if (filteredHistory.length === 0) return { data: [], hasPrediction: false }; + + const actualPoints = [...filteredHistory] + .reverse() + .map(h => ({ + recorded_at: h.recorded_at, + timestamp: new Date(h.recorded_at).getTime(), + cumulative_exp: BOND_EXP_TABLE[h.bond_level] || 0, + bond_level: h.bond_level, + isPrediction: false, + actual_exp: BOND_EXP_TABLE[h.bond_level] || 0, + predicted_exp: null as number | null, + })); + + const lastActual = actualPoints[actualPoints.length - 1]; + const targetExp = 240225; + + if (lastActual.cumulative_exp >= targetExp || actualPoints.length < 2) { + return { data: actualPoints, hasPrediction: false, xDomain: ['dataMin', 'dataMax'] as const }; + } + + const n = actualPoints.length; + let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0; + for (const p of actualPoints) { + sumX += p.timestamp; + sumY += p.cumulative_exp; + sumXY += p.timestamp * p.cumulative_exp; + sumXX += p.timestamp * p.timestamp; + } + + const denominator = n * sumXX - sumX * sumX; + if (denominator === 0) { + return { data: actualPoints, hasPrediction: false, xDomain: ['dataMin', 'dataMax'] as const }; + } + + const slope = (n * sumXY - sumX * sumY) / denominator; + const intercept = (sumY - slope * sumX) / n; + + if (slope <= 0) { + return { data: actualPoints, hasPrediction: false, xDomain: ['dataMin', 'dataMax'] as const }; + } + + const predictionPoints: any[] = []; + + predictionPoints.push({ + recorded_at: lastActual.recorded_at, + timestamp: lastActual.timestamp, + cumulative_exp: lastActual.cumulative_exp, + bond_level: lastActual.bond_level, + isPrediction: true, + actual_exp: null, + predicted_exp: lastActual.cumulative_exp + }); + + let nextTargetLevel = Math.floor((lastActual.bond_level / 5) + 1) * 5; + if (nextTargetLevel === lastActual.bond_level) { + nextTargetLevel += 5; + } + + for (let lv = nextTargetLevel; lv <= 100; lv += 5) { + const expAtLv = BOND_EXP_TABLE[lv]; + if (expAtLv === undefined) continue; + + const ts = Math.round((expAtLv - intercept) / slope); + + predictionPoints.push({ + recorded_at: format(new Date(ts), 'yyyy-MM-dd'), + timestamp: ts, + cumulative_exp: expAtLv, + bond_level: lv, + isPrediction: true, + actual_exp: null, + predicted_exp: expAtLv + }); + } + + const finalPoint = predictionPoints[predictionPoints.length - 1]; + if (!finalPoint || finalPoint.bond_level !== 100) { + const ts100 = Math.round((targetExp - intercept) / slope); + predictionPoints.push({ + recorded_at: format(new Date(ts100), 'yyyy-MM-dd'), + timestamp: ts100, + cumulative_exp: targetExp, + bond_level: 100, + isPrediction: true, + actual_exp: null, + predicted_exp: targetExp + }); + } + + return { + data: [...actualPoints, ...predictionPoints.slice(0)], + hasPrediction: true, + xDomain: [actualPoints[0].timestamp, predictionPoints[predictionPoints.length - 1].timestamp] as const + }; + }, [filteredHistory]); + + const graphData = chartDataCombined.data; + + useEffect(() => { + if (!isMounted) return; + localStorage.setItem('last_selected_char_id', selectedCharId); + const charHistory = bondHistory + .filter(h => h.char_key === selectedCharId) + .sort((a, b) => b.recorded_at.localeCompare(a.recorded_at)); + if (charHistory.length > 0) { + setInputLevel(Math.min(100, charHistory[0].bond_level + 1)); + } else { + setInputLevel(1); + } + }, [selectedCharId, bondHistory, isMounted]); + + useEffect(() => { + const handleResize = () => { + setIsDesktop(window.innerWidth >= 1000); + }; + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsSelectOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleSave = async () => { + await onSave(inputLevel, inputDate, selectedCharId); + }; + + const handleDelete = async (recordId: string) => { + if (!window.confirm("この記録を削除してもよろしいですか?")) return; + + try { + let token = ''; + if (typeof window !== 'undefined') { + const authKey = Object.keys(localStorage).find(key => key.startsWith('sb-') && key.endsWith('-auth-token')); + if (authKey) { + const authDataString = localStorage.getItem(authKey); + if (authDataString) { + const authData = JSON.parse(authDataString); + token = authData?.access_token || ''; + } + } + } + + const res = await fetch('/api/relationship', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ id: recordId }), + }); + + if (!res.ok) throw new Error('Failed to delete'); + + window.location.reload(); + } catch (err) { + console.error(err); + alert("削除に失敗しました。"); + } + }; + + const renderGraph = ( + <div + ref={containerRef} + className={`graph-container ${!isChartReady ? 'opacity-0 pointer-events-none' : ''}`} + data-chart-ready={isChartReady ? "true" : "false"} + > + {isChartReady && ( + graphData.length > 0 ? ( + <ResponsiveContainer width="100%" height="100%"> + <AreaChart data={graphData} margin={{ top: 5, right: 5, left: -35, bottom: -10 }}> + <defs> + <linearGradient id="colorExp" x1="0" y1="0" x2="0" y2="1"> + <stop offset="5%" stopColor="var(--primary)" stopOpacity={0.4}/> + <stop offset="60%" stopColor="var(--primary)" stopOpacity={0.2}/> + <stop offset="95%" stopColor="var(--primary)" stopOpacity={0}/> + </linearGradient> + </defs> + <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--secondary)" opacity={0.2} /> + <XAxis + dataKey="timestamp" + type="number" + domain={chartDataCombined.xDomain || ['dataMin', 'dataMax']} + tickLine={true} + axisLine={true} + stroke="var(--secondary-foreground)" + fontSize={10} + tickCount={14} + tickFormatter={(ts) => { + try { + const d = new Date(ts); + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + return `${y}/${m}`; + } catch (e) {} + return ''; + }} + /> + <YAxis + dataKey="cumulative_exp" + domain={[0, 240225]} + ticks={[0, 14790, 29175, 50835, 81270, 121980, 174465, 240225]} + tickFormatter={(exp) => `${getLevelFromExp(exp)}`} + stroke="var(--secondary-foreground)" + fontSize={10} + tickLine={true} + axisLine={true} + /> + <Tooltip + labelFormatter={(_, payload) => { + if (payload && payload[0]) { + const p = payload[0].payload; + return p.isPrediction ? `Estimated Date: ${p.recorded_at}` : `Date: ${p.recorded_at}`; + } + return ''; + }} + formatter={(value: any, name: any, props: any) => { + const p = props.payload; + + if (p.isPrediction) { + if (name === 'predicted_exp') { + return [`RANK ${p.bond_level} (Est. Target)`]; + } + return null as any; + } + + if (name === 'actual_exp') { + return [`RANK ${p.bond_level} (${p.actual_exp} exp)`]; + } + + return null as any; + }} + contentStyle={{ + background: 'var(--card)', + border: '1px solid var(--muted)', + borderRadius: '0.5rem', + fontSize: '0.85rem', + }} + labelStyle={{ + color: 'var(--foreground)', + fontWeight: 700 + }} + /> + <Area + type="monotone" + dataKey="actual_exp" + stroke="var(--primary)" + strokeWidth={3} + fillOpacity={1} + fill="url(#colorExp)" + connectNulls={false} + /> + <Line + type="monotone" + dataKey="predicted_exp" + stroke="var(--primary)" + strokeWidth={2} + strokeDasharray="5 5" + dot={false} + activeDot={false} + connectNulls={true} + /> + </AreaChart> + </ResponsiveContainer> + ) : ( + <div className="flex h-full w-full items-center justify-center border border-dashed border-(--muted) rounded-2xl bg-(--card)/30"> + <span className="text-lg font-bold text-(--muted-foreground) tracking-wider opacity-60"> + No Data + </span> + </div> + ) + )} + </div> + ); + + return ( + <div className="bond-container"> + <div className="bond-split-layout"> + <div className="bond-left-panel"> + <div className="char-selector" ref={dropdownRef}> + <button + className="char-selector-trigger" + onClick={() => setIsSelectOpen(!isSelectOpen)} + > + <span className="char-selector-label">Student</span> + <h1 className="char-selector-name"> + {selectedChar.name} + <span className={`char-selector-arrow ${isSelectOpen ? 'open' : ''}`}> + ▼ + </span> + </h1> + </button> + + {isSelectOpen && ( + <div className="char-dropdown scrollbar-hide animate-in fade-in zoom-in-95 duration-200"> + <div className="char-dropdown-container"> + {CHARACTER_LIST.map((char) => ( + <button + key={char.id} + onClick={() => { + setSelectedCharId(char.id as CharacterId); + setIsSelectOpen(false); + }} + className={`char-dropdown-item ${selectedCharId === char.id ? 'active' : ''}`} + > + {char.name} + </button> + ))} + </div> + </div> + )} + </div> + + <div className="record-input-area"> + <div className="input-group-container"> + <div className="input-field-wrapper"> + <label className="input-label">Rank</label> + <div className="input-content"> + <span className="rank-value">{inputLevel}</span> + <div className="spin-buttons-box"> + <button + onClick={() => setInputLevel(prev => Math.min(100, prev + 1))} + className="spin-btn up" + > + ▲ + </button> + <button + onClick={() => setInputLevel(prev => Math.max(1, prev - 1))} + className="spin-btn down" + > + ▼ + </button> + </div> + </div> + </div> + + <div className="input-field-wrapper"> + <label className="input-label">Date</label> + <div className="input-content"> + <input + type="date" + value={inputDate} + onChange={(e) => setInputDate(e.target.value)} + className="date-input-display" + /> + </div> + </div> + </div> + + <button + onClick={handleSave} + disabled={isSyncing} + className="btn-bond-record" + > + <span>Record</span> + <div>+</div> + </button> + </div> + + <div className="section-wrapper"> + <h3 className="gift-title">Favorite Gifts</h3> + <div className="gift-scroll-container scrollbar-hide"> + {sortedGifts.map((gift) => { + const rating = selectedChar.giftRatings[gift.name]; + const rankChar = rating?.slice(-1).toUpperCase(); + const row = Math.floor(gift.spriteIdx / SPRITE_CONFIG.cols); + const col = gift.spriteIdx % SPRITE_CONFIG.cols; + + return ( + <div key={gift.name} className="gift-item"> + <div className="gift-image-box"> + <div + className="gift-sprite" + style={{ '--col': col, '--row': row } as React.CSSProperties} + /> + <div className={`gift-rank-dot ${ + rankChar === 'S' ? 's' : rankChar === 'A' ? 'a' : 'b' + }`} /> + </div> + <span className="gift-category-text"> + {gift.category || "GIFT"} + </span> + </div> + ); + })} + </div> + </div> + + <div className="section-wrapper"> + <h3 className="section-toggle-btn" style={{ cursor: 'default' }}> + <span>History Log</span> + </h3> + <div className="history-grid-container"> + {displayHistoryRows.map((record, index) => ( + <div key={record ? record.id : `empty-${index}`} className="history-item"> + <div className="history-log-row"> + {record ? ( + <> + <span className="history-rank-num"> + {record.bond_level} + </span> + <span className="history-divider">|</span> + <span className="history-date-text"> + {record.recorded_at} + </span> + </> + ) : ( + <span className="history-empty-text"> + <span className="history-rank-num"> + --- + </span> + <span className="history-divider">|</span> + <span className="history-date-text"> + ---- -- -- + </span> + </span> + )} + </div> + + {record ? ( + <button + onClick={() => handleDelete(record.id)} + className="edit-icon-btn text-rose-500 hover:text-rose-700 hover:bg-rose-500/10" + aria-label="Delete record" + > + <TrashIcon /> + </button> + ) : ( + <span className="edit-btn-placeholder" /> + )} + </div> + ))} + </div> + </div> + + {!isDesktop && ( + <div className="section-wrapper"> + <h3 className="section-toggle-btn" style={{ cursor: 'default' }}> + <span>Progress Graph</span> + </h3> + <div className="animate-in fade-in duration-200"> + {renderGraph} + </div> + </div> + )} + </div> + {isDesktop && ( + <div className="bond-right-panel animate-in fade-in duration-300"> + <h3 className="section-toggle-btn" style={{ cursor: 'default' }}>Progress Graph</h3> + {renderGraph} + </div> + )} + </div> + </div> + ); +} + +if (typeof window !== "undefined") { + const filterWarning = (args: any[], originalFn: (...args: any[]) => void) => { + if ( + typeof args[0] === "string" && + args[0].includes("should be greater than 0") + ) { + return; + } + originalFn(...args); + }; + + const originalConsoleError = console.error; + console.error = (...args: any[]) => filterWarning(args, originalConsoleError); + const originalConsoleWarn = console.warn; + console.warn = (...args: any[]) => filterWarning(args, originalConsoleWarn); +}+ \ No newline at end of file diff --git a/src/components/BottomNavBar.tsx b/src/components/BottomNavBar.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { Timer, CalendarDays } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import { Timer, Heart } from 'lucide-react'; -type Tab = 'timer' | 'history'; +type Tab = 'timer' | 'history' | 'bond'; interface BottomNavBarProps { activeTab: Tab; @@ -9,30 +9,93 @@ interface BottomNavBarProps { } const BottomNavBar: React.FC<BottomNavBarProps> = ({ activeTab, setActiveTab }) => { - const tabs = [ - { id: 'timer' as const, icon: Timer }, - { id: 'history' as const, icon: CalendarDays }, - ]; + const [isDesktop, setIsDesktop] = useState(false); + + useEffect(() => { + const handleResize = () => { + setIsDesktop(window.innerWidth >= 1000); + }; + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); return ( - <div className="fixed bottom-0 left-0 right-0 desktop-hidden"> - {tabs.map((tab) => { - const Icon = tab.icon; - const isActive = activeTab === tab.id; - - return ( + <div className="fixed bottom-0 left-0 right-0 z-40 flex border-t border-muted/10 bg-background"> + {isDesktop ? ( + /* ========================================== + PC用レイアウト: 2タブ構成 + ========================================== */ + <> + <button + onClick={() => setActiveTab('timer')} + className={`btn-nav relative flex flex-col items-center justify-center ${ + activeTab === 'timer' || activeTab === 'history' ? 'active' : '' + }`} + > + <div className="relative flex flex-col items-center"> + <Timer size={30} /> + <div className={`btn-nav-bar absolute rounded-full ${ + activeTab === 'timer' || activeTab === 'history' ? 'active' : '' + }`}/> + </div> + </button> + + <button + onClick={() => setActiveTab('bond')} + className={`btn-nav relative flex flex-col items-center justify-center ${ + activeTab === 'bond' ? 'active' : '' + }`} + > + <div className="relative flex flex-col items-center"> + <Heart size={30} /> + <div className={`btn-nav-bar absolute rounded-full ${ + activeTab === 'bond' ? 'active' : '' + }`}/> + </div> + </button> + </> + ) : ( + /* ========================================== + スマホ用レイアウト: 3タブ構成 + ========================================== */ + <> + <button + onClick={() => setActiveTab('timer')} + className={`btn-nav relative flex flex-col items-center justify-center ${activeTab === 'timer' ? 'active' : ''}`} + > + <div className="relative flex flex-col items-center"> + <Timer size={30} /> + <div className={`btn-nav-bar absolute rounded-full ${activeTab === 'timer' ? 'active' : ''}`}/> + </div> + </button> + + <button + onClick={() => setActiveTab('history')} + className={`btn-nav relative flex flex-col items-center justify-center ${activeTab === 'history' ? 'active' : ''}`} + > + <div className="relative flex flex-col items-center"> + <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"> + <rect width="18" height="18" x="3" y="4" rx="2" ry="2"/> + <line x1="16" x2="16" y1="2" y2="6"/> + <line x1="8" x2="8" y1="2" y2="6"/> + <line x1="3" x2="21" y1="10" y2="10"/> + </svg> + <div className={`btn-nav-bar absolute rounded-full ${activeTab === 'history' ? 'active' : ''}`}/> + </div> + </button> + <button - key={tab.id} - onClick={() => setActiveTab(tab.id)} - className={`btn-nav relative flex flex-col items-center justify-center ${isActive ? 'active' : ''}`} + onClick={() => setActiveTab('bond')} + className={`btn-nav relative flex flex-col items-center justify-center ${activeTab === 'bond' ? 'active' : ''}`} > <div className="relative flex flex-col items-center"> - <Icon size={30} /> - <div className={`btn-nav-bar absolute rounded-full ${isActive ? 'active' : ''}`}/> + <Heart size={30} /> + <div className={`btn-nav-bar absolute rounded-full ${activeTab === 'bond' ? 'active' : ''}`}/> </div> </button> - ); - })} + </> + )} </div> ); }; diff --git a/src/components/NewFeatureDialog.tsx b/src/components/NewFeatureDialog.tsx @@ -0,0 +1,81 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; + +export default function NewFeatureDialog() { + const [isOpen, setIsOpen] = useState(false); + const [isChecked, setIsChecked] = useState(false); + + useEffect(() => { + if (typeof window !== 'undefined') { + const isNotified = localStorage.getItem('new_feature_notified'); + if (!isNotified) { + setIsOpen(true); + } + } + }, []); + + const handleClose = () => { + if (isChecked) { + localStorage.setItem('new_feature_notified', 'true'); + } + setIsOpen(false); + }; + + if (!isOpen) return null; + + return ( + <div className="fixed inset-0 bg-black/40 dark:bg-black/60 z-100 flex justify-center items-center p-4 animate-in fade-in duration-200 backdrop-blur-sm"> + <div className="w-full max-w-sm rounded-2xl shadow-xl border border-dashed bg-(--card) border-(--muted) flex flex-col overflow-hidden animate-in zoom-in-95 duration-200"> + + <div className="border-b border-dashed flex justify-between items-center p-5"> + <h2 className="text-xl font-bold text-(--foreground) tracking-wide flex items-center gap-2"> + <span>✨</span> 新機能追加のお知らせ + </h2> + </div> + + <div className="p-6 flex flex-col gap-4 text-sm font-medium leading-relaxed text-(--secondary-foreground)"> + <p>いつもご利用いただきありがとうございます!</p> + + <div className="bg-(--background) p-4 rounded-xl border border-dashed border-(--muted)/60 flex flex-col gap-3"> + <div className="flex items-start gap-2"> + <span className="text-base">▶</span> + <div> + <strong className="text-(--foreground) block font-bold">絆上げ記録機能</strong> + <span className="text-xs">生徒さんごとに絆上げの記録をつけられるようになりました。日付とランクを入力して登録できます。</span> + </div> + </div> + <div className="flex items-start gap-2"> + <span className="text-base">▶</span> + <div> + <strong className="text-(--foreground) block font-bold">絆ランク100の未来予想線</strong> + <span className="text-xs">現在の進捗ペースから絆ランク100に到達する日付を予測して、グラフに破線で描画します。</span> + </div> + </div> + </div> + </div> + + {/* 下部アクションエリア(チェックボックスとボタン) */} + <div className="p-4 bg-(--background)/50 border-t border-dashed border-(--muted)/40 flex flex-col gap-3"> + <label className="flex items-center gap-2 px-1 cursor-pointer select-none text-xs text-(--secondary-foreground) font-bold"> + <input + type="checkbox" + checked={isChecked} + onChange={(e) => setIsChecked(e.target.checked)} + className="w-4 h-4 rounded border-(--muted) text-(--primary) focus:ring-(--primary) cursor-pointer" + /> + <span>今後このお知らせを表示しない</span> + </label> + + <button + onClick={handleClose} + className="w-full py-3 rounded-xl font-bold bg-(--primary) text-(--primary-foreground) transition-all active:scale-98 cursor-pointer shadow-sm hover:brightness-105" + > + 閉じる + </button> + </div> + + </div> + </div> + ); +}+ \ No newline at end of file diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx @@ -40,7 +40,7 @@ const Settings = ({ onOpenContent }: SettingsProps) => { { label: 'FAQ', key: 'faq' }, { label: '利用規約', key: 'terms' }, { label: 'プライバシーポリシー', key: 'privacy' }, - { label: '運営者情報・変更履歴', key: 'operator' }, + { label: '運営者情報など', key: 'operator' }, ]; const handleNotificationClick = async () => { diff --git a/src/components/TimerDashboard.tsx b/src/components/TimerDashboard.tsx @@ -124,7 +124,7 @@ export default function TimerDashboard({ return ( <div className="dashboard-container"> <div className="timer-card"> - <div className="flex items-center justify-between mb-4"> + <div className="flex items-center justify-between mb-2"> <h2 className="timer-card-title">Next Tap</h2> </div> @@ -174,7 +174,7 @@ export default function TimerDashboard({ <h2 className="timer-card-title mb-4">Next Call</h2> <div className="grid grid-cols-2 gap-4"> <div className="flex flex-col gap-2"> - <div className="countdown-text-s bg-background/50 p-2 rounded"> + <div className="countdown-text-s bg-background/50 pt-1 pl-1 rounded"> <CountdownDisplay milliseconds={ticket1Remaining} /> </div> <button @@ -184,7 +184,7 @@ export default function TimerDashboard({ ><span>{ isSyncing ? 'Wait...' : 'Ticket 1' }</span></button> </div> <div className="flex flex-col gap-2"> - <div className="countdown-text-s bg-background/50 p-2 rounded"> + <div className="countdown-text-s bg-background/50 pt-1 pl-1 rounded"> <CountdownDisplay milliseconds={ticket2Remaining} /> </div> <button diff --git a/src/components/pages.tsx b/src/components/pages.tsx @@ -17,6 +17,7 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React <li>生徒さんの訪問タイミング(4:00/16:00 JST)のカウントダウン</li> <li>招待券のクールタイム管理</li> <li>タップ履歴のカレンダー表示</li> + <li>絆上げの記録とグラフ表示</li> </ul> <h2 className="text-xl font-bold mt-8 mb-4 text-foreground">対応プラットフォーム</h2> <p> @@ -73,7 +74,7 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React <div className="bg-blue-500/10 border border-blue-500/50 rounded-lg p-4 text-sm mb-4"> <p className="font-bold mb-2">ℹ️ お願い</p> <p> - 通知登録されている端末数が増加すると、運営者の大人のカードの出番がやってきてしまいます。 + 通知登録されている端末数が増加すると、運営者の大人のカードの出番がやってくるかもしれません。 </p> <p className="mt-1"> 通知登録は1ユーザーあたり2端末までを目安とし、通知が不要な端末は都度登録解除していただくよう、何卒ご協力をお願いいたします。 @@ -103,7 +104,7 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React <h3 className="font-bold text-foreground"> Q: 記録を間違えてタップしてしまいました。削除できますか?</h3> <p className="font-normal text-sm"> - A: Tap履歴を個別に編集・削除することはできません。データベースへの接続増加を伴うため、現時点では実装予定はありません。</p> + A: カフェタッチのTap履歴は個別に編集・削除することはできません。絆ランク記録は直近10件のデータまで削除可能です。</p> </div> <div className="flex flex-col gap-1"> @@ -141,14 +142,14 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React <h3 className="font-bold text-foreground"> Q: オフラインでも利用できますか?</h3> <p className="font-normal text-sm"> - A: 利用できません。インターネットに接続が必要です。</p> + A: 利用できません。インターネットへの接続が必要です。</p> </div> <div className="flex flex-col gap-1"> <h3 className="font-bold text-foreground"> Q: ゲーム内部のデータを使用していますか?</h3> <p className="font-normal text-sm"> - A: 本サイトはゲーム「ブルーアーカイブ」内のいかなるアセットも使用していません。また、ゲームデータにもアクセスしていませんので、本サイトがゲーム内のアカウントに影響を与えることはありません。</p> + A: 本サイトはゲーム「ブルーアーカイブ」内に実装されている機能などは使用していません。また、ゲームデータ自体にもアクセスしていませんので、本サイトがゲーム内のアカウントに影響を与えることはありません。</p> </div> <div className="flex flex-col gap-1"> @@ -280,9 +281,10 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React ), }, operator: { - title: "運営者情報・変更履歴", + title: "運営者情報など", body: (() => { const updateLogs = [ + { date: '2026-06-07', content: '絆ランク記録機能追加' }, { date: '2026-02-08', content: '初回リリース' }, ]; @@ -302,24 +304,28 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React ハチか<span className="ml-2 mr-1">X:</span><a href="https://x.com/bite_sour_sweet" target="_blank" rel="noopener noreferrer" className="hover:underline text-blue-400">@bite_sour_sweet</a> </p> </div> + <div className="flex flex-col gap-2"> + <h3 className="font-bold text-foreground border-b border-muted pb-1">謝辞</h3> + <p className="font-normal"> + ブルーアーカイブ(ブルアカ)攻略 Wiki 様(絆ランクテーブル・生徒さんごとの贈り物対応付けに使用)<span className="ml-2 mr-1">HP:</span><a href="https://bluearchive.wikiru.jp/" target="_blank" rel="noopener noreferrer" className="hover:underline text-blue-400">https://bluearchive.wikiru.jp/</a> + </p> + </div> </div> - - <div className="flex flex-col gap-1 pb-2"> + <div className="flex flex-col gap-2"> <h3 className="font-bold text-foreground border-b border-muted pb-1">変更履歴</h3> <div className="bg-background/50 rounded-md p-1.5 max-h-48 overflow-y-auto space-y-2 text-sm"> {updateLogs.map((log, index) => ( - <div key={index} className="flex gap-3 border-b border-muted/30 pb-1.5 last:border-0"> - <span className="font-mono shrink-0">{log.date}</span> + <div key={index} className="flex gap-3"> + <span className="shrink-0">{log.date}</span> <p className="text-foreground/80">{log.content}</p> </div> ))} </div> </div> - <div className="flex flex-col gap-2"> <h3 className="font-bold text-foreground border-b border-muted pb-1">連絡先・フィードバック</h3> <p className="text-sm leading-relaxed"> - 不具合報告やご要望は、 + 不具合・ミスのご報告やご要望は、 <a href="https://github.com/Sunny-JP/ba-cafe" target="_blank" rel="noopener noreferrer" className="mx-1 hover:underline text-blue-400"> GitHub </a> diff --git a/src/lib/bondData.ts b/src/lib/bondData.ts @@ -0,0 +1,3267 @@ +// 絆レベル累計EXPテーブル +export const BOND_EXP_TABLE: Record<number, number> = { + 1: 0, 2: 15, 3: 45, 4: 75, 5: 110, + 6: 145, 7: 180, 8: 220, 9: 260, 10: 300, + 11: 360, 12: 450, 13: 555, 14: 675, 15: 815, + 16: 975, 17: 1155, 18: 1360, 19: 1590, 20: 1845, + 21: 2130, 22: 2445, 23: 2790, 24: 3165, 25: 3575, + 26: 4020, 27: 4500, 28: 5020, 29: 5580, 30: 6180, + 31: 6825, 32: 7515, 33: 8250, 34: 9030, 35: 9860, + 36: 10740, 37: 11670, 38: 12655, 39: 13695, 40: 14790, + 41: 15945, 42: 17160, 43: 18435, 44: 19770, 45: 21170, + 46: 22635, 47: 24165, 48: 25765, 49: 27435, 50: 29175, + 51: 30990, 52: 32880, 53: 34845, 54: 36885, 55: 39005, + 56: 41205, 57: 43485, 58: 45850, 59: 48300, 60: 50835, + 61: 53460, 62: 56175, 63: 58980, 64: 61875, 65: 64865, + 66: 67950, 67: 71130, 68: 74410, 69: 77790, 70: 81270, + 71: 84855, 72: 88545, 73: 92340, 74: 96240, 75: 100250, + 76: 104370, 77: 108600, 78: 112945, 79: 117405, 80: 121980, + 81: 126675, 82: 131490, 83: 136425, 84: 141480, 85: 146660, + 86: 151965, 87: 157395, 88: 162955, 89: 168645, 90: 174465, + 91: 180420, 92: 186510, 93: 192735, 94: 199095, 95: 205595, + 96: 212235, 97: 219015, 98: 225940, 99: 233010, 100: 240225 +}; + +export const SPRITE_CONFIG = { + cols: 5, // 1行あたりのコマ数 + width: 142, // 1コマの横幅 + height: 112, // 1コマの高さ +}; + +export const GIFT_EXP = { + nb: 1.0, // 通常 効果中 + na: 1.5, // 通常 効果大 + ns: 2.0, // 通常 効果特大 + sa: 1.5, // 高級 効果大 + ss: 2.0, // 高級 効果特大 +} as const; + +export const GIFT_LIST = [ + { name: 'レースの枕', category:'煌めき', baseExp: 120, spriteIdx: 0 }, + { name: 'ストリートオブヤンキー1巻', category:'煌めき', baseExp: 120, spriteIdx: 1 }, + { name: 'サミュエラ「ザ・ビヨンド」', category:'煌めき', baseExp: 120, spriteIdx: 2 }, + { name: 'レトロな卵の工芸品', category:'煌めき', baseExp: 120, spriteIdx: 3 }, + { name: 'ミルフィーユの正統派パフェ', category:'煌めき', baseExp: 120, spriteIdx: 4 }, + { name: 'エイ~ブックレア', category:'煌めき', baseExp: 120, spriteIdx: 5 }, + { name: 'おしゃれなくし', category:'煌めき', baseExp: 120, spriteIdx: 6 }, + { name: '栄養満載の総合ビタミンゼリー', category:'煌めき', baseExp: 120, spriteIdx: 7 }, + { name: '最高級の楓の盆栽', category:'煌めき', baseExp: 120, spriteIdx: 8 }, + { name: '裁縫キット', category:'煌めき', baseExp: 120, spriteIdx: 9 }, + { name: '音楽演奏会入場券', category:'煌めき', baseExp: 120, spriteIdx: 10 }, + { name: '1日3回ダンベルセット', category:'煌めき', baseExp: 120, spriteIdx: 11 }, + { name: 'ボードゲーム「ザ・人生」', category:'煌めき',baseExp: 120, spriteIdx: 12 }, + + { name: 'ウェーブキャットの枕', category:'牡丹', baseExp: 40, spriteIdx: 20 }, + { name: 'ペロロの腹筋ローラー', category:'アサガオ', baseExp: 40, spriteIdx: 21 }, + { name: 'エーポッドプロ', category:'薔薇', baseExp: 40, spriteIdx: 22 }, + { name: '禁断の愛~許されないからこそ美しく~', category:'チューリップ', baseExp: 40, spriteIdx: 23 }, + { name: 'ゲームマガジン「ヒットガールズ」', category:'チューリップ', baseExp: 40, spriteIdx: 24 }, + { name: 'チェリーローズカラーのグロス', category:'桜', baseExp: 40, spriteIdx: 25 }, + { name: 'お肌を透明にするBBクリーム', category:'桜', baseExp: 40, spriteIdx: 26 }, + { name: 'ミリタリー用カモフラージュクリーム3種セット', category:'桜', baseExp: 40, spriteIdx: 27 }, + { name: '高級なクッキーセット', category:'スイセン', baseExp: 40, spriteIdx: 28 }, + { name: '『銃 可愛い 青春』', category:'チューリップ', baseExp: 40, spriteIdx: 29 }, + { name: '天体望遠鏡', category:'百日紅', baseExp: 40, spriteIdx: 30 }, + { name: 'ゲームガールカラー復刻版', category:'薔薇', baseExp: 40, spriteIdx: 31 }, + { name: 'MX-レーションC型デザート風味', category:'スイセン', baseExp: 40, spriteIdx: 32 }, + { name: '抹茶味の瓶ラムネ', category:'スイセン', baseExp: 40, spriteIdx: 33 }, + { name: 'ゼリーズの枕', category:'牡丹', baseExp: 40, spriteIdx: 34 }, + { name: '跳躍探偵ウサギ~霧に包まれた温泉での滑落~', category:'チューリップ', baseExp: 40, spriteIdx: 35 }, + { name: 'O-フィット', category:'アサガオ', baseExp: 40, spriteIdx: 36 }, + { name: '埋蔵金の地図', category:'タンポポ', baseExp: 40, spriteIdx: 37 }, + { name: 'コスプレ用ぐるぐるメガネ', category:'百日紅', baseExp: 40, spriteIdx: 38 }, + { name: '世界で最も無駄な絡繰りボックス', category:'タンポポ', baseExp: 40, spriteIdx: 39 }, + { name: 'リボンのついた熊のぬいぐるみ', category:'百日紅', baseExp: 40, spriteIdx: 40 }, + { name: '映画の前売りペアチケット', category:'ユリ', baseExp: 40, spriteIdx: 41 }, + { name: '30色の絵の具セット', category:'百日紅', baseExp: 40, spriteIdx: 42 }, + { name: '古典の詩集', category:'チューリップ', baseExp: 40, spriteIdx: 43 }, + { name: '可愛い食器セット', category:'キンセンカ', baseExp: 40, spriteIdx: 44 }, + { name: '頭脳開発キューブパズル', category:'百日紅', baseExp: 40, spriteIdx: 45 }, + { name: '大きなホールケーキ', category:'スイセン', baseExp: 40, spriteIdx: 46 }, + { name: 'ハイクラスビュッフェ招待券', category:'ユリ', baseExp: 40, spriteIdx: 47 }, + { name: '食虫植物の植木鉢', category:'桔梗', baseExp: 40, spriteIdx: 48 }, + { name: '高級そうな欲望のつぼ', category:'タンポポ', baseExp: 40, spriteIdx: 49 }, + { name: 'ぜんまい式オルゴール', category:'タンポポ', baseExp: 40, spriteIdx: 50 }, + { name: '刺繍付きのハンカチ', category:'モクレン', baseExp: 40, spriteIdx: 51 }, + { name: '百科事典', category:'チューリップ', baseExp: 40, spriteIdx: 52 }, + { name: 'ザ・サプリメント', category:'ツツジ', baseExp: 40, spriteIdx: 53 }, + { name: '夏模様の浮き輪', category:'翡翠花', baseExp: 40, spriteIdx: 54 } +] as const; + +export const CHARACTER_LIST = [ + { + id: 'airi', + name: 'アイリ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '高級なクッキーセット': 'na', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'airi_band', + name: 'アイリ(バンド)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '高級なクッキーセット': 'na', + '大きなホールケーキ': 'na', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'aoba', + name: 'アオバ', + giftRatings: { + '裁縫キット': 'ss', + '栄養満載の総合ビタミンゼリー': 'sa', + '音楽演奏会入場券': 'sa', + 'ハイクラスビュッフェ招待券': 'na', + '映画の前売りペアチケット': 'nb', + '可愛い食器セット': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'akane', + name: 'アカネ', + giftRatings: { + '裁縫キット': 'sa', + '可愛い食器セット': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'akane_bunnygirl', + name: 'アカネ(バニーガール)', + giftRatings: { + '裁縫キット': 'ss', + '可愛い食器セット': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'akari', + name: 'アカリ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + 'ハイクラスビュッフェ招待券': 'na', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'akari_newyear', + name: 'アカリ(正月)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + 'ハイクラスビュッフェ招待券': 'na', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'ako', + name: 'アコ', + giftRatings: { + 'レトロな卵の工芸品': 'sa', + 'ザ・サプリメント': 'na', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'ako_dress', + name: 'アコ(ドレス)', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + 'レトロな卵の工芸品': 'sa', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'na', + 'ザ・サプリメント': 'na', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'azusa', + name: 'アズサ', + giftRatings: { + 'おしゃれなくし': 'sa', + 'リボンのついた熊のぬいぐるみ': 'na', + 'ウェーブキャットの枕': 'nb', + 'ペロロの腹筋ローラー': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'azusa_swimsuit', + name: 'アズサ(水着)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'リボンのついた熊のぬいぐるみ': 'na', + '夏模様の浮き輪': 'na', + 'ウェーブキャットの枕': 'nb', + 'ペロロの腹筋ローラー': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'asuna', + name: 'アスナ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'チェリーローズカラーのグロス': 'na', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'asuna_bunnygirl', + name: 'アスナ(バニーガール)', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + 'ミルフィーユの正統派パフェ': 'sa', + '大きなホールケーキ': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'asuna_schooluniform', + name: 'アスナ(制服)', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + 'チェリーローズカラーのグロス': 'na', + 'お肌を透明にするBBクリーム': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'atsuko', + name: 'アツコ', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + '百科事典': 'na', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'atsuko_swimsuit', + name: 'アツコ(水着)', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '古典の詩集': 'na', + '百科事典': 'na', + '夏模様の浮き輪': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'ayane', + name: 'アヤネ', + giftRatings: { + 'ストリートオブヤンキー1巻': 'sa', + '『銃 可愛い 青春』': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'ayane_swimsuit', + name: 'アヤネ(水着)', + giftRatings: { + 'エイ~ブックレア': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '『銃 可愛い 青春』': 'na', + '夏模様の浮き輪': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'arisu', + name: 'アリス', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'ゲームマガジン「ヒットガールズ」': 'na', + 'ゲームガールカラー復刻版': 'nb', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'arisu_maid', + name: 'アリス(メイド)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'ボードゲーム「ザ・人生」': 'ss', + 'ゲームマガジン「ヒットガールズ」': 'na', + '頭脳開発キューブパズル': 'na', + '天体望遠鏡': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'O-フィット': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'arisu_battle', + name: 'アリス(臨戦)', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'おしゃれなくし': 'sa', + 'ゲームガールカラー復刻版': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '天体望遠鏡': 'nb', + 'O-フィット': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'aru', + name: 'アル', + giftRatings: { + 'レトロな卵の工芸品': 'sa', + '埋蔵金の地図': 'na', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'aru_newyear', + name: 'アル(正月)', + giftRatings: { + 'レトロな卵の工芸品': 'sa', + '埋蔵金の地図': 'na', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'aru_dress', + name: 'アル(ドレス)', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + '埋蔵金の地図': 'na', + '世界で最も無駄な絡繰りボックス': 'na', + '高級そうな欲望のつぼ': 'na', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'iori', + name: 'イオリ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + 'MX-レーションC型デザート風味': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + '高級なクッキーセット': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'iori_swimsuit', + name: 'イオリ(水着)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + 'MX-レーションC型デザート風味': 'na', + '夏模様の浮き輪': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + '高級なクッキーセット': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'izuna', + name: 'イズナ', + giftRatings: { + 'おしゃれなくし': 'sa', + '刺繍付きのハンカチ': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'izuna_swimsuit', + name: 'イズナ(水着)', + giftRatings: { + 'おしゃれなくし': 'sa', + '刺繍付きのハンカチ': 'na', + '夏模様の浮き輪': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'izumi', + name: 'イズミ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '抹茶味の瓶ラムネ': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'izumi_swimsuit', + name: 'イズミ(水着)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '抹茶味の瓶ラムネ': 'na', + '夏模様の浮き輪': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'izumi_newyear', + name: 'イズミ(正月)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '抹茶味の瓶ラムネ': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'ichika', + name: 'イチカ', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + 'エイ~ブックレア': 'sa', + 'おしゃれなくし': 'sa', + 'チェリーローズカラーのグロス': 'na', + 'エーポッドプロ': 'nb', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'ゲームガールカラー復刻版': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'ichika_swimsuit', + name: 'イチカ(水着)', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + 'エイ~ブックレア': 'sa', + 'チェリーローズカラーのグロス': 'na', + '夏模様の浮き輪': 'na', + 'エーポッドプロ': 'nb', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'ゲームガールカラー復刻版': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'ibuki', + name: 'イブキ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + 'ボードゲーム「ザ・人生」': 'ss', + '高級なクッキーセット': 'na', + 'リボンのついた熊のぬいぐるみ': 'na', + '30色の絵の具セット': 'na', + '天体望遠鏡': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '頭脳開発キューブパズル': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'iroha', + name: 'イロハ', + giftRatings: { + 'ストリートオブヤンキー1巻': 'sa', + 'ゲームマガジン「ヒットガールズ」': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + '『銃 可愛い 青春』': 'nb', + 'ゲームガールカラー復刻版 ': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + 'O-フィット': 'nb', + '古典の詩集': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'ui', + name: 'ウイ', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '『銃 可愛い 青春』': 'na', + '古典の詩集': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'ui_swimsuit', + name: 'ウイ(水着)', + giftRatings: { + 'ストリートオブヤンキー1巻': 'ss', + '『銃 可愛い 青春』': 'na', + '埋蔵金の地図': 'na', + '古典の詩集': 'na', + '夏模様の浮き輪': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'utaha', + name: 'ウタハ', + giftRatings: { + 'エイ~ブックレア': 'ss', + '天体望遠鏡': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'utaha_cheerleader', + name: 'ウタハ(応援団)', + giftRatings: { + 'エイ~ブックレア': 'ss', + '天体望遠鏡': 'na', + 'エーポッドプロ': 'nb', + 'ゲームガールカラー復刻版': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'umika', + name: 'ウミカ', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'コスプレ用ぐるぐるメガネ': 'na', + '天体望遠鏡': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'eimi', + name: 'エイミ', + giftRatings: { + 'レトロな卵の工芸品': 'sa', + 'ぜんまい式オルゴール': 'na', + 'エーポッドプロ': 'nb', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'eimi_swimsuit', + name: 'エイミ(水着)', + giftRatings: { + 'レトロな卵の工芸品': 'sa', + 'ぜんまい式オルゴール': 'na', + '夏模様の浮き輪': 'na', + 'エーポッドプロ': 'nb', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'eimi_battle', + name: 'エイミ(臨戦)', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + '禁断の愛~許されないからこそ美しく~': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'O-フィット': 'nb', + '埋蔵金の地図': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'eri', + name: 'エリ', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + '埋蔵金の地図': 'na', + '30色の絵の具セット': 'na', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'erika', + name: 'エリカ', + giftRatings: { + 'おしゃれなくし': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'リボンのついた熊のぬいぐるみ': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'チェリーローズカラーのグロス': 'nb', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'otogi', + name: 'オトギ', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + '1日3回ダンベルセット': 'sa', + 'O-フィット': 'na', + 'ペロロの腹筋ローラー': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '埋蔵金の地図': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kaede', + name: 'カエデ', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'sa', + 'コスプレ用ぐるぐるメガネ': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '天体望遠鏡': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'O-フィット': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '食虫植物の植木鉢': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kazusa', + name: 'カズサ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '高級なクッキーセット': 'na', + 'チェリーローズカラーのグロス': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kazusa_band', + name: 'カズサ(バンド)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '高級なクッキーセット': 'na', + '大きなホールケーキ': 'na', + 'エーポッドプロ': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kasumi', + name: 'カスミ', + giftRatings: { + 'エイ~ブックレア': 'ss', + '埋蔵金の地図': 'na', + '天体望遠鏡': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kanoe', + name: 'カノエ', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + 'エーポッドプロ': 'na', + '禁断の愛~許されないからこそ美しく~': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kaho', + name: 'カホ', + giftRatings: { + 'ストリートオブヤンキー1巻': 'ss', + '古典の詩集': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kayoko', + name: 'カヨコ', + giftRatings: { + 'エイ~ブックレア': 'sa', + 'エーポッドプロ': 'na', + 'ゲームガールカラー復刻版': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kayoko_newyear', + name: 'カヨコ(正月)', + giftRatings: { + '音楽演奏会入場券': 'ss', + 'エイ~ブックレア': 'sa', + 'おしゃれなくし': 'sa', + 'エーポッドプロ': 'na', + '刺繍付きのハンカチ': 'na', + 'ゲームガールカラー復刻版': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kayoko_dress', + name: 'カヨコ(ドレス)', + giftRatings: { + 'エイ~ブックレア': 'ss', + 'エーポッドプロ': 'na', + 'ぜんまい式オルゴール': 'na', + 'ゲームガールカラー復刻版': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'karin', + name: 'カリン', + giftRatings: { + '裁縫キット': 'ss', + '頭脳開発キューブパズル': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'karin_schooluniform', + name: 'カリン(制服)', + giftRatings: { + '裁縫キット': 'ss', + '可愛い食器セット': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'karin_bunnygirl', + name: 'カリン(バニーガール)', + giftRatings: { + '裁縫キット': 'ss', + 'リボンのついた熊のぬいぐるみ': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kanna', + name: 'カンナ', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + 'ザ・サプリメント': 'na', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kanna_swimsuit', + name: 'カンナ(水着)', + giftRatings: { + 'ストリートオブヤンキー1巻': 'ss', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'na', + '夏模様の浮き輪': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '古典の詩集': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kikyou', + name: 'キキョウ', + giftRatings: { + 'ストリートオブヤンキー1巻': 'ss', + '『銃 可愛い 青春』': 'na', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'na', + '古典の詩集': 'na', + '百科事典': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kikyou_swimsuit', + name: 'キキョウ(水着)', + giftRatings: { + 'ストリートオブヤンキー1巻': 'ss', + '『銃 可愛い 青春』': 'na', + '百科事典': 'na', + '夏模様の浮き輪': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kisaki', + name: 'キサキ', + giftRatings: { + '最高級の楓の盆栽': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '可愛い食器セット': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '古典の詩集': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kirara', + name: 'キララ', + giftRatings: { + 'おしゃれなくし': 'ss', + 'チェリーローズカラーのグロス': 'na', + 'お肌を透明にするBBクリームス': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kirino', + name: 'キリノ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '刺繍付きのハンカチ': 'na', + '高級なクッキーセット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kirino_swimsuit', + name: 'キリノ(水着)', + giftRatings: { + '1日3回ダンベルセット': 'ss', + 'O-フィット': 'na', + '夏模様の浮き輪': 'na', + 'ペロロの腹筋ローラー': 'nb', + '高級なクッキーセット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kurumi', + name: 'クルミ', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'お肌を透明にするBBクリーム': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kei', + name: 'ケイ', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + 'チェリーローズカラーのグロス': 'na', + 'ゲームガールカラー復刻版': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kokona', + name: 'ココナ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '高級なクッキーセット': 'na', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kotama', + name: 'コタマ', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'sa', + 'リボンのついた熊のぬいぐるみ': 'na', + '天体望遠鏡': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kotama_camping', + name: 'コタマ(キャンプ)', + giftRatings: { + 'エイ~ブックレア': 'ss', + 'ボードゲーム「ザ・人生」': 'sa', + 'リボンのついた熊のぬいぐるみ': 'na', + '天体望遠鏡': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kotori', + name: 'コトリ', + giftRatings: { + 'エイ~ブックレア': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '百科事典': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'kotori_cheerleader', + name: 'コトリ(応援団)', + giftRatings: { + '1日3回ダンベルセット': 'ss', + '百科事典': 'na', + 'ペロロの腹筋ローラー': 'nb', + 'O-フィット': 'nb', + '頭脳開発キューブパズル': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'konoka', + name: 'コノカ', + giftRatings: { + '1日3回ダンベルセット': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + 'レースの枕': 'sa', + 'ペロロの腹筋ローラー': 'na', + 'ウェーブキャットの枕': 'nb', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + 'ゼリーズの枕': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + 'O-フィット': 'nb', + '古典の詩集': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'koharu', + name: 'コハル', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'sa', + '禁断の愛~許されないからこそ美しく~': 'na', + 'チェリーローズカラーのグロス': 'na', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'koharu_swimsuit', + name: 'コハル(水着)', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + '禁断の愛~許されないからこそ美しく~': 'na', + '夏模様の浮き輪': 'na', + '『銃 可愛い 青春』': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'koyuki', + name: 'コユキ', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + '世界で最も無駄な絡繰りボックス': 'na', + '頭脳開発キューブパズル': 'na', + '天体望遠鏡': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'koyuki_pajama', + name: 'コユキ(パジャマ)', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'レースの枕': 'sa', + 'ウェーブキャットの枕': 'na', + 'ゼリーズの枕': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'saori', + name: 'サオリ', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'チェリーローズカラーのグロス': 'na', + 'お肌を透明にするBBクリーム': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'saori_swimsuit', + name: 'サオリ(水着)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'チェリーローズカラーのグロス': 'na', + 'お肌を透明にするBBクリーム': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'na', + '夏模様の浮き輪': 'na', + 'MX-レーションC型デザート風味': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'saori_dress', + name: 'サオリ(ドレス)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'チェリーローズカラーのグロス': 'na', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'saki', + name: 'サキ', + giftRatings: { + 'レトロな卵の工芸品': 'sa', + 'ミリタリー用カモフラージュクリーム3種セット': 'na', + 'MX-レーションC型デザート風味': 'nb', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + '百科事典': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'saki_swimsuit', + name: 'サキ(水着)', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + 'レトロな卵の工芸品': 'sa', + 'ミリタリー用カモフラージュクリーム3種セット': 'na', + '夏模様の浮き輪': 'na', + 'MX-レーションC型デザート風味': 'nb', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + '百科事典': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'sakurako', + name: 'サクラコ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '大きなホールケーキ': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'sakurako_idol', + name: 'サクラコ(アイドル)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + 'ザ・サプリメント': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '古典の詩集': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'satsuki', + name: 'サツキ', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '百科事典': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '古典の詩集': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'saya', + name: 'サヤ', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'sa', + 'ザ・サプリメント': 'na', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'saya_casual', + name: 'サヤ(私服)', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + 'コスプレ用ぐるぐるメガネ': 'na', + '高級なクッキーセット': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'sigure', + name: 'シグレ', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + 'ザ・サプリメント': 'na', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'sigure_hotspring', + name: 'シグレ(温泉)', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + 'ザ・サプリメント': 'na', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'shizuko', + name: 'シズコ', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'sa', + 'ザ・サプリメント': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'shizuko_swimsuit', + name: 'シズコ(水着)', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'sa', + 'ザ・サプリメント': 'na', + '夏模様の浮き輪': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'shimiko', + name: 'シミコ', + giftRatings: { + 'ストリートオブヤンキー1巻': 'sa', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '古典の詩集': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'juri', + name: 'ジュリ', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + '30色の絵の具セット': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'juri_parttimer', + name: 'ジュリ(アルバイト)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '刺繍付きのハンカチ': 'na', + 'チェリーローズカラーのグロス': 'nb', + '可愛い食器セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'shun', + name: 'シュン', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'お肌を透明にするBBクリーム': 'na', + 'チェリーローズカラーのグロス': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'shun_kid', + name: 'シュン(幼女)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '高級なクッキーセット': 'na', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'junko', + name: 'ジュンコ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '高級なクッキーセット': 'na', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'junko_newyear', + name: 'ジュンコ(正月)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '高級なクッキーセット': 'na', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'siroko', + name: 'シロコ', + giftRatings: { + '1日3回ダンベルセット': 'sa', + 'ペロロの腹筋ローラー': 'na', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'siroko_riding', + name: 'シロコ(ライディング)', + giftRatings: { + '1日3回ダンベルセット': 'sa', + '埋蔵金の地図': 'na', + 'ペロロの腹筋ローラー': 'nb', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'siroko_swimsuit', + name: 'シロコ(水着)', + giftRatings: { + '1日3回ダンベルセット': 'ss', + 'ペロロの腹筋ローラー': 'na', + '夏模様の浮き輪': 'na', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'siroko_terror', + name: 'シロコ*テラー', + giftRatings: { + '1日3回ダンベルセット': 'sa', + 'ペロロの腹筋ローラー': 'na', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'suzumi', + name: 'スズミ', + giftRatings: { + '音楽演奏会入場券': 'ss', + 'リボンのついた熊のぬいぐるみ': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'suzumi_magical', + name: 'スズミ(マジカル)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'レースの枕': 'sa', + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'エイ~ブックレア': 'sa', + 'エーポッドプロ': 'na', + 'お肌を透明にするBBクリーム': 'na', + 'ウェーブキャットの枕': 'nb', + 'チェリーローズカラーのグロス': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'ゼリーズの枕': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'subaru', + name: 'スバル', + giftRatings: { + '音楽演奏会入場券': 'ss', + 'レトロな卵の工芸品': 'sa', + '埋蔵金の地図': 'na', + '大きなホールケーキ': 'na', + 'ぜんまい式オルゴール': 'na', + 'エーポッドプロ': 'nb', + 'チェリーローズカラーのグロス': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'sumire', + name: 'スミレ', + giftRatings: { + '1日3回ダンベルセット': 'sa', + 'O-フィット': 'na', + 'ペロロの腹筋ローラー': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'sumire_parttimer', + name: 'スミレ(アルバイト)', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + '1日3回ダンベルセット': 'sa', + 'O-フィット': 'na', + 'ペロロの腹筋ローラー': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'seia', + name: 'セイア', + giftRatings: { + 'ストリートオブヤンキー1巻': 'ss', + '古典の詩集': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'seia_swimsuit', + name: 'セイア(水着)', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + 'ゲームガールカラー復刻版': 'na', + '夏模様の浮き輪': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '古典の詩集': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'sena', + name: 'セナ', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + 'レースの枕': 'sa', + 'ウェーブキャットの枕': 'na', + 'ゼリーズの枕': 'nb', + '百科事典': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'sena_casual', + name: 'セナ(私服)', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + 'ザ・サプリメント': 'na', + 'コスプレ用ぐるぐるメガネ': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'serika', + name: 'セリカ', + giftRatings: { + 'レトロな卵の工芸品': 'sa', + '高級そうな欲望のつぼ': 'na', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'serika_newyear', + name: 'セリカ(正月)', + giftRatings: { + 'レトロな卵の工芸品': 'sa', + '高級そうな欲望のつぼ': 'na', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'serika_swimsuit', + name: 'セリカ(水着)', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + '高級そうな欲望のつぼ': 'na', + '夏模様の浮き輪': 'na', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + 'ぜんまい式オルゴール': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'serina', + name: 'セリナ', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + '栄養満載の総合ビタミンゼリー': 'ss', + 'ゼリーズの枕': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'serina_christmas', + name: 'セリナ(クリスマス)', + giftRatings: { + 'レースの枕': 'ss', + '栄養満載の総合ビタミンゼリー': 'sa', + 'ゼリーズの枕': 'na', + 'ウェーブキャットの枕': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'takane', + name: 'タカネ', + giftRatings: { + '音楽演奏会入場券': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '『銃 可愛い 青春』': 'na', + '古典の詩集': 'na', + '刺繍付きのハンカチ': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'chiaki', + name: 'チアキ', + giftRatings: { + 'エイ~ブックレア': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '古典の詩集': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'cherino', + name: 'チェリノ', + giftRatings: { + 'レトロな卵の工芸品': 'sa', + '高級そうな欲望のつぼ': 'na', + '埋蔵金の地図': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '大きなホールケーキ': 'nb', + '食虫植物の植木鉢': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'cherino_hotspring', + name: 'チェリノ(温泉)', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + '埋蔵金の地図': 'na', + 'コスプレ用ぐるぐるメガネ': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'chise', + name: 'チセ', + giftRatings: { + 'ストリートオブヤンキー1巻': 'sa', + '古典の詩集': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'chise_swimsuit', + name: 'チセ(水着)', + giftRatings: { + 'ストリートオブヤンキー1巻': 'sa', + '古典の詩集': 'na', + '夏模様の浮き輪': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'chinatsu', + name: 'チナツ', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '『銃 可愛い 青春』': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'chinatsu_hotspring', + name: 'チナツ(温泉)', + giftRatings: { + 'ストリートオブヤンキー1巻': 'sa', + 'レトロな卵の工芸品': 'sa', + '『銃 可愛い 青春』': 'na', + 'ぜんまい式オルゴール': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '古典の詩集': 'nb', + '高級そうな欲望のつぼ': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'chihiro', + name: 'チヒロ', + giftRatings: { + 'エイ~ブックレア': 'sa', + 'エーポッドプロ': 'na', + 'ゲームガールカラー復刻版': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'tsukuyo', + name: 'ツクヨ', + giftRatings: { + '最高級の楓の盆栽': 'sa', + '食虫植物の植木鉢': 'na', + 'コスプレ用ぐるぐるメガネ': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'tsukuyo_dress', + name: 'ツクヨ(ドレス)', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + 'おしゃれなくし': 'sa', + 'お肌を透明にするBBクリーム': 'na', + 'チェリーローズカラーのグロス': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'tsubaki', + name: 'ツバキ', + giftRatings: { + '最高級の楓の盆栽': 'ss', + 'レースの枕': 'sa', + 'ウェーブキャットの枕': 'na', + 'ゼリーズの枕': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'tsubaki_guide', + name: 'ツバキ(ガイド)', + giftRatings: { + 'レースの枕': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + 'ウェーブキャットの枕': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + 'ゼリーズの枕': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'tsurugi', + name: 'ツルギ', + giftRatings: { + 'おしゃれなくし': 'ss', + '禁断の愛~許されないからこそ美しく~': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'tsurugi_swimsuit', + name: 'ツルギ(水着)', + giftRatings: { + 'おしゃれなくし': 'ss', + '禁断の愛~許されないからこそ美しく~': 'na', + '夏模様の浮き輪': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'toki', + name: 'トキ', + giftRatings: { + '1日3回ダンベルセット': 'ss', + 'ペロロの腹筋ローラー': 'na', + 'O-フィット': 'nb', + '百科事典': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'toki_bunnygirl', + name: 'トキ(バニーガール)', + giftRatings: { + '裁縫キット': 'ss', + '1日3回ダンベルセット': 'ss', + 'ペロロの腹筋ローラー': 'na', + 'O-フィット': 'nb', + '百科事典': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'toki_battle', + name: 'トキ(臨戦)', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'エイ~ブックレア': 'sa', + 'ゲームガールカラー復刻版': 'na', + '映画の前売りペアチケット': 'na', + 'エーポッドプロ': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'O-フィット': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'tomoe', + name: 'トモエ', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + '古典の詩集': 'na', + '『銃 可愛い 青春』': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'tomoe_qipao', + name: 'トモエ(チーパオ)', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + 'おしゃれなくし': 'sa', + 'チェリーローズカラーのグロス': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + '刺繍付きのハンカチ': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'nagisa', + name: 'ナギサ', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + 'ぜんまい式オルゴール': 'na', + '高級なクッキーセット': 'nb', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'nagisa_swimsuit', + name: 'ナギサ(水着)', + giftRatings: { + 'レースの枕': 'ss', + 'レトロな卵の工芸品': 'sa', + '高級なクッキーセット': 'na', + '夏模様の浮き輪': 'na', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'nagusa', + name: 'ナグサ', + giftRatings: { + 'おしゃれなくし': 'ss', + 'ミルフィーユの正統派パフェ': 'sa', + '古典の詩集': 'na', + 'お肌を透明にするBBクリーム': 'nb', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'natsu', + name: 'ナツ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '埋蔵金の地図': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'natsu_band', + name: 'ナツ(バンド)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '高級なクッキーセット': 'na', + '大きなホールケーキ': 'na', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '埋蔵金の地図': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'niko', + name: 'ニコ', + giftRatings: { + '裁縫キット': 'ss', + 'ミルフィーユの正統派パフェ': 'sa', + 'MX-レーションC型デザート風味': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + '高級なクッキーセット': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '可愛い食器セット': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'niya', + name: 'ニヤ', + giftRatings: { + '最高級の楓の盆栽': 'ss', + 'レトロな卵の工芸品': 'sa', + 'ボードゲーム「ザ・人生」': 'sa', + '禁断の愛~許されないからこそ美しく~': 'na', + 'コスプレ用ぐるぐるメガネ': 'na', + '高級そうな欲望のつぼ': 'na', + '天体望遠鏡': 'nb', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '食虫植物の植木鉢': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'neru', + name: 'ネル', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'sa', + 'コスプレ用ぐるぐるメガネ': 'na', + '天体望遠鏡': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'neru_schooluniform', + name: 'ネル(制服)', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'コスプレ用ぐるぐるメガネ': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '天体望遠鏡': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'O-フィット': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '食虫植物の植木鉢': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'neru_bunnygirl', + name: 'ネル(バニーガール)', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'ゲームガールカラー復刻版': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'noa', + name: 'ノア', + giftRatings: { + 'ストリートオブヤンキー1巻': 'sa', + '古典の詩集': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'noa_pajama', + name: 'ノア(パジャマ)', + giftRatings: { + 'レースの枕': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + 'サミュエラ「ザ・ビヨンド」': 'sa', + '古典の詩集': 'na', + 'ウェーブキャットの枕': 'nb', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'チェリーローズカラーのグロス': 'nb', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + '『銃 可愛い 青春』': 'nb', + 'ゼリーズの枕': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'nozomi', + name: 'ノゾミ', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'レトロな卵の工芸品': 'sa', + 'コスプレ用ぐるぐるメガネ': 'na', + '天体望遠鏡': 'nb', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'nodoka', + name: 'ノドカ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '天体望遠鏡': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'nodoka_hotspring', + name: 'ノドカ(温泉)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '音楽演奏会入場券': 'sa', + 'ハイクラスビュッフェ招待券': 'na', + '高級なクッキーセット': 'nb', + '天体望遠鏡': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '映画の前売りペアチケット': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'nonomi', + name: 'ノノミ', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'チェリーローズカラーのグロス': 'na', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'nonomi_swimsuit', + name: 'ノノミ(水着)', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'チェリーローズカラーのグロス': 'na', + '夏模様の浮き輪': 'na', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hasumi', + name: 'ハスミ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '大きなホールケーキ': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + 'ハイクラスビュッフェ招待券': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hasumi_sportswear', + name: 'ハスミ(体操服)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '大きなホールケーキ': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + 'ハイクラスビュッフェ招待券': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hasumi_swimsuit', + name: 'ハスミ(水着)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '大きなホールケーキ': 'na', + '夏模様の浮き輪': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + 'ハイクラスビュッフェ招待券': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hanae', + name: 'ハナエ', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'お肌を透明にするBBクリーム': 'na', + 'チェリーローズカラーのグロス': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hanae_christmas', + name: 'ハナエ(クリスマス)', + giftRatings: { + '1日3回ダンベルセット': 'ss', + '栄養満載の総合ビタミンゼリー': 'sa', + 'O-フィット': 'na', + 'ペロロの腹筋ローラー': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hanako', + name: 'ハナコ', + giftRatings: { + 'ストリートオブヤンキー1巻': 'sa', + '禁断の愛~許されないからこそ美しく~': 'na', + '世界で最も無駄な絡繰りボックス': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hanako_swimsuit', + name: 'ハナコ(水着)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'お肌を透明にするBBクリーム': 'ns', + '夏模様の浮き輪': 'na', + 'チェリーローズカラーのグロス': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'haruka', + name: 'ハルカ', + giftRatings: { + '最高級の楓の盆栽': 'sa', + '食虫植物の植木鉢': 'na', + 'コスプレ用ぐるぐるメガネ': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'haruka_newyear', + name: 'ハルカ(正月)', + giftRatings: { + '最高級の楓の盆栽': 'ss', + '食虫植物の植木鉢': 'na', + 'コスプレ用ぐるぐるメガネ': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'ハルカ_ドレス', + name: 'ハルカ(ドレス)', + giftRatings: { + '最高級の楓の盆栽': 'ss', + 'レトロな卵の工芸品': 'sa', + 'ボードゲーム「ザ・人生」': 'sa', + '食虫植物の植木鉢': 'na', + '天体望遠鏡': 'nb', + '埋蔵金の地図': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'haruna', + name: 'ハルナ', + giftRatings: { + '音楽演奏会入場券': 'ss', + '刺繍付きのハンカチ': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'haruna_newyear', + name: 'ハルナ(正月)', + giftRatings: { + '音楽演奏会入場券': 'ss', + '高級なクッキーセット': 'na', + '刺繍付きのハンカチ': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'haruna_sportswear', + name: 'ハルナ(体操服)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '1日3回ダンベルセット': 'sa', + '高級なクッキーセット': 'na', + 'ペロロの腹筋ローラー': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + 'O-フィット': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hare', + name: 'ハレ', + giftRatings: { + 'エイ~ブックレア': 'ss', + 'ゲームマガジン「ヒットガールズ」': 'na', + 'ゲームガールカラー復刻版': 'nb', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hare_camping', + name: 'ハレ(キャンプ)', + giftRatings: { + 'エイ~ブックレア': 'ss', + 'ゲームマガジン「ヒットガールズ」': 'na', + 'ゲームガールカラー復刻版': 'nb', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hikari', + name: 'ヒカリ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '高級なクッキーセット': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + 'O-フィット': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hina', + name: 'ヒナ', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + '世界で最も無駄な絡繰りボックス': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hina_swimsuit', + name: 'ヒナ(水着)', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + '世界で最も無駄な絡繰りボックス': 'na', + '夏模様の浮き輪': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hina_dress', + name: 'ヒナ(ドレス)', + giftRatings: { + '音楽演奏会入場券': 'ss', + '世界で最も無駄な絡繰りボックス': 'na', + '映画の前売りペアチケット': 'na', + 'ハイクラスビュッフェ招待券': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hinata', + name: 'ヒナタ', + giftRatings: { + '裁縫キット': 'ss', + '最高級の楓の盆栽': 'sa', + '食虫植物の植木鉢': 'na', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hinata_swimsuit', + name: 'ヒナタ(水着)', + giftRatings: { + '裁縫キット': 'ss', + '可愛い食器セット': 'na', + '夏模様の浮き輪': 'na', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hibiki', + name: 'ヒビキ', + giftRatings: { + 'エイ~ブックレア': 'sa', + 'エーポッドプロ': 'na', + 'ゲームガールカラー復刻版': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hibiki_cheerleader', + name: 'ヒビキ(応援団)', + giftRatings: { + '裁縫キット': 'ss', + 'エイ~ブックレア': 'sa', + 'エーポッドプロ': 'na', + 'ゲームガールカラー復刻版': 'nb', + '可愛い食器セット': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hifumi', + name: 'ヒフミ', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + 'ペロロの腹筋ローラー': 'na', + 'ウェーブキャットの枕': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hifumi_swimsuit', + name: 'ヒフミ(水着)', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + 'ペロロの腹筋ローラー': 'na', + '夏模様の浮き輪': 'na', + 'ウェーブキャットの枕': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'himari', + name: 'ヒマリ', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + '世界で最も無駄な絡繰りボックス': 'na', + '埋蔵金の地図': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'himari_battle', + name: 'ヒマリ(臨戦)', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + '世界で最も無駄な絡繰りボックス': 'na', + '埋蔵金の地図': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + '百科事典': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hiyori', + name: 'ヒヨリ', + giftRatings: { + '裁縫キット': 'sa', + '可愛い食器セット': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hiyori_swimsuit', + name: 'ヒヨリ(水着)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '禁断の愛~許されないからこそ美しく~': 'na', + '大きなホールケーキ': 'na', + '夏模様の浮き輪': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + 'ハイクラスビュッフェ招待券': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'pina', + name: 'フィーナ', + giftRatings: { + '裁縫キット': 'ss', + '映画の前売りペアチケット': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'pina_guide', + name: 'フィーナ(ガイド)', + giftRatings: { + 'ストリートオブヤンキー1巻': 'ss', + '音楽演奏会入場券': 'sa', + '映画の前売りペアチケット': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + 'ハイクラスビュッフェ招待券': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'fuuka', + name: 'フウカ', + giftRatings: { + '裁縫キット': 'sa', + '可愛い食器セット': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'fuuka_newyear', + name: 'フウカ(正月)', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + '裁縫キット': 'sa', + '可愛い食器セット': 'na', + 'ザ・サプリメント': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'fubuki', + name: 'フブキ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '高級なクッキーセット': 'na', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'fubuki_swimsuit', + name: 'フブキ(水着)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '高級なクッキーセット': 'na', + '抹茶味の瓶ラムネ': 'na', + '夏模様の浮き輪': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'MX-レーションC型デザート風味': 'nb', + 'O-フィット': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'fuyu', + name: 'フユ', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + '映画の前売りペアチケット': 'na', + 'チェリーローズカラーのグロス': 'nb', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hoshino', + name: 'ホシノ', + giftRatings: { + 'レースの枕': 'ss', + 'レトロな卵の工芸品': 'ss', + '埋蔵金の地図': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hoshino_swimsuit', + name: 'ホシノ(水着)', + giftRatings: { + 'レースの枕': 'ss', + 'レトロな卵の工芸品': 'ss', + '埋蔵金の地図': 'na', + '夏模様の浮き輪': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hoshino_battle', + name: 'ホシノ(臨戦)', + giftRatings: { + 'レースの枕': 'ss', + 'レトロな卵の工芸品': 'sa', + 'ミリタリー用カモフラージュクリーム3種セット': 'na', + 'MX-レーションC型デザート風味': 'na', + '埋蔵金の地図': 'na', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'maki', + name: 'マキ', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'sa', + '30色の絵の具セット': 'na', + '天体望遠鏡': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '頭脳開発キューブパズル': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'maki_camping', + name: 'マキ(キャンプ)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'エイ~ブックレア': 'sa', + 'ゲームガールカラー復刻版 ': 'na', + '30色の絵の具セット': 'na', + 'エーポッドプロ': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'makoto', + name: 'マコト', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + '埋蔵金の地図': 'na', + '世界で最も無駄な絡繰りボックス': 'na', + '高級そうな欲望のつぼ': 'na', + 'コスプレ用ぐるぐるメガネ': 'nb', + '食虫植物の植木鉢': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mashiro', + name: 'マシロ', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + 'ミリタリー用カモフラージュクリーム3種セット': 'na', + 'MX-レーションC型デザート風味': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mashiro_swimsuit', + name: 'マシロ(水着)', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + 'MX-レーションC型デザート風味': 'na', + '夏模様の浮き輪': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mari', + name: 'マリー', + giftRatings: { + '最高級の楓の盆栽': 'sa', + '刺繍付きのハンカチ': 'na', + '食虫植物の植木鉢': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mari_sportswear', + name: 'マリー(体操服)', + giftRatings: { + 'おしゃれなくし': 'ss', + '最高級の楓の盆栽': 'sa', + '刺繍付きのハンカチ': 'na', + '食虫植物の植木鉢': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mari_idol', + name: 'マリー(アイドル)', + giftRatings: { + '裁縫キット': 'ss', + '可愛い食器セット': 'na', + '刺繍付きのハンカチ': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'marina', + name: 'マリナ', + giftRatings: { + 'レトロな卵の工芸品': 'sa', + 'リボンのついた熊のぬいぐるみ': 'na', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'marina_qipao', + name: 'マリナ(チーパオ)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'レトロな卵の工芸品': 'sa', + 'ボードゲーム「ザ・人生」': 'sa', + 'リボンのついた熊のぬいぐるみ': 'na', + '天体望遠鏡': 'nb', + '埋蔵金の地図': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mika', + name: 'ミカ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'お肌を透明にするBBクリーム': 'na', + 'チェリーローズカラーのグロス': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mika_swimsuit', + name: 'ミカ(水着)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'チェリーローズカラーのグロス': 'na', + '夏模様の浮き輪': 'na', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'misaki', + name: 'ミサキ', + giftRatings: { + 'レースの枕': 'ss', + 'リボンのついた熊のぬいぐるみ': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'misaki_swimsuit', + name: 'ミサキ(水着)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'リボンのついた熊のぬいぐるみ': 'na', + '夏模様の浮き輪': 'na', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'michiru', + name: 'ミチル', + giftRatings: { + 'レトロな卵の工芸品': 'sa', + '埋蔵金の地図': 'na', + '世界で最も無駄な絡繰りボックス': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'michiru_dress', + name: 'ミチル(ドレス)', + giftRatings: { + 'おしゃれなくし': 'ss', + '埋蔵金の地図': 'na', + '高級そうな欲望のつぼ': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '食虫植物の植木鉢': 'nb', + '刺繍付きのハンカチ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'midori', + name: 'ミドリ', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'ゼリーズの枕': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'midori_maid', + name: 'ミドリ(メイド)', + giftRatings: { + '裁縫キット': 'ss', + '可愛い食器セット': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mina', + name: 'ミナ', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + '高級そうな欲望のつぼ': 'na', + 'ぜんまい式オルゴール': 'na', + '埋蔵金の地図': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '食虫植物の植木鉢': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mine', + name: 'ミネ', + giftRatings: { + 'サミュエラ「ザ・ビヨンド」': 'ss', + 'チェリーローズカラーのグロス': 'na', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mine_idol', + name: 'ミネ(アイドル)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'ウェーブキャットの枕': 'na', + 'チェリーローズカラーのグロス': 'na', + 'リボンのついた熊のぬいぐるみ': 'na', + 'ペロロの腹筋ローラー': 'nb', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'minori', + name: 'ミノリ', + giftRatings: { + '栄養満載の総合ビタミンゼリー': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '『銃 可愛い 青春』': 'na', + '百科事典': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + 'ザ・サプリメント': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mimori', + name: 'ミモリ', + giftRatings: { + 'おしゃれなくし': 'sa', + '裁縫キット': 'sa', + '可愛い食器セット': 'na', + '刺繍付きのハンカチ': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mimori_swimsuit', + name: 'ミモリ(水着)', + giftRatings: { + '裁縫キット': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + 'サミュエラ「ザ・ビヨンド」': 'sa', + '古典の詩集': 'na', + '夏模様の浮き輪': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'チェリーローズカラーのグロス': 'nb', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '可愛い食器セット': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'miyako', + name: 'ミヤコ', + giftRatings: { + 'エイ~ブックレア': 'ss', + '『銃 可愛い 青春』': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'miyako_swimsuit', + name: 'ミヤコ(水着)', + giftRatings: { + 'レースの枕': 'ss', + '『銃 可愛い 青春』': 'na', + '夏模様の浮き輪': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'miyu', + name: 'ミユ', + giftRatings: { + 'おしゃれなくし': 'ss', + '刺繍付きのハンカチ': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'miyu_swimsuit', + name: 'ミユ(水着)', + giftRatings: { + 'おしゃれなくし': 'ss', + '刺繍付きのハンカチ': 'na', + '夏模様の浮き輪': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'miyo', + name: 'ミヨ', + giftRatings: { + 'ストリートオブヤンキー1巻': 'ss', + 'レトロな卵の工芸品': 'sa', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '古典の詩集': 'nb', + '高級そうな欲望のつぼ': 'nb', + 'ぜんまい式オルゴール': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mutsuki', + name: 'ムツキ', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'sa', + 'コスプレ用ぐるぐるメガネ': 'ns', + '天体望遠鏡': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'mutsuki_newyear', + name: 'ムツキ(正月)', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'sa', + 'コスプレ用ぐるぐるメガネ': 'na', + '天体望遠鏡': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'ムツキ_ドレス', + name: 'ムツキ(ドレス)', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'コスプレ用ぐるぐるメガネ': 'na', + '天体望遠鏡': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'megu', + name: 'メグ', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'コスプレ用ぐるぐるメガネ': 'na', + '天体望遠鏡': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'meru', + name: 'メル', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '30色の絵の具セット': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '天体望遠鏡': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '古典の詩集': 'nb', + '頭脳開発キューブパズル': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'moe', + name: 'モエ', + giftRatings: { + 'エイ~ブックレア': 'sa', + 'エーポッドプロ': 'na', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'MX-レーションC型デザート風味': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'moe_swimsuit', + name: 'モエ(水着)', + giftRatings: { + 'エイ~ブックレア': 'ss', + '夏模様の浮き輪': 'na', + 'エーポッドプロ': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'MX-レーションC型デザート風味': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'momiji', + name: 'モミジ', + giftRatings: { + 'ストリートオブヤンキー1巻': 'ss', + '禁断の愛~許されないからこそ美しく~': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'チェリーローズカラーのグロス': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'momoi', + name: 'モモイ', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'ゲームガールカラー復刻版': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'momoi_maid', + name: 'モモイ(メイド)', + giftRatings: { + 'ボードゲーム「ザ・人生」': 'ss', + 'レースの枕': 'sa', + 'ゲームガールカラー復刻版': 'na', + 'ウェーブキャットの枕': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'ゼリーズの枕': 'nb', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'yakumo', + name: 'ヤクモ', + giftRatings: { + 'レトロな卵の工芸品': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '高級そうな欲望のつぼ': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '古典の詩集': 'nb', + 'ぜんまい式オルゴール': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'yuuka', + name: 'ユウカ', + giftRatings: { + 'エイ~ブックレア': 'ss', + '栄養満載の総合ビタミンゼリー': 'ss', + '頭脳開発キューブパズル': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'yuuka_sportswear', + name: 'ユウカ(体操服)', + giftRatings: { + '頭脳開発キューブパズル': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'yuuka_pajama', + name: 'ユウカ(パジャマ)', + giftRatings: { + 'レースの枕': 'ss', + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'エイ~ブックレア': 'sa', + 'ウェーブキャットの枕': 'na', + 'エーポッドプロ': 'nb', + 'チェリーローズカラーのグロス': 'nb', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'ゼリーズの枕': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'yukari', + name: 'ユカリ', + giftRatings: { + 'おしゃれなくし': 'ss', + 'ミルフィーユの正統派パフェ': 'sa', + '刺繍付きのハンカチ': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'yukari_swimsuit', + name: 'ユカリ(水着)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'ミルフィーユの正統派パフェ': 'sa', + '刺繍付きのハンカチ': 'na', + '夏模様の浮き輪': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'yuzu', + name: 'ユズ', + giftRatings: { + 'エイ~ブックレア': 'sa', + 'ゲームガールカラー復刻版': 'na', + 'エーポッドプロ': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'yuzu_maid', + name: 'ユズ(メイド)', + giftRatings: { + 'エイ~ブックレア': 'ss', + 'ゲームガールカラー復刻版': 'na', + 'エーポッドプロ': 'nb', + 'ゲームマガジン「ヒットガールズ」': 'nb', + 'O-フィット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'yuzu_battle', + name: 'ユズ(臨戦)', + giftRatings: { + 'エイ~ブックレア': 'ss', + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'ミリタリー用カモフラージュクリーム3種セット': 'na', + 'ゲームガールカラー復刻版': 'na', + 'チェリーローズカラーのグロス': 'nb', + 'お肌を透明にするBBクリーム': 'nb', + 'MX-レーションC型デザート風味': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'yoshimi', + name: 'ヨシミ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'sa', + '高級なクッキーセット': 'na', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'yoshimi_band', + name: 'ヨシミ(バンド)', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + '高級なクッキーセット': 'na', + '大きなホールケーキ': 'na', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + 'チェリーローズカラーのグロス': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'rabu', + name: 'ラブ', + giftRatings: { + '1日3回ダンベルセット': 'ss', + 'レトロな卵の工芸品': 'sa', + 'ミルフィーユの正統派パフェ': 'sa', + 'ペロロの腹筋ローラー': 'na', + '高級なクッキーセット': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + 'O-フィット': 'nb', + '埋蔵金の地図': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '大きなホールケーキ': 'nb', + '高級そうな欲望のつぼ': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'rio', + name: 'リオ', + giftRatings: { + 'エイ~ブックレア': 'ss', + '天体望遠鏡': 'na', + 'エーポッドプロ': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'rio_battle', + name: 'リオ(臨戦)', + giftRatings: { + 'エイ~ブックレア': 'ss', + '天体望遠鏡': 'na', + 'エーポッドプロ': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + '世界で最も無駄な絡繰りボックス': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'ritsu', + name: 'リツ', + giftRatings: { + '最高級の楓の盆栽': 'ss', + 'ボードゲーム「ザ・人生」': 'sa', + 'コスプレ用ぐるぐるメガネ': 'na', + '天体望遠鏡': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '頭脳開発キューブパズル': 'nb', + '食虫植物の植木鉢': 'nb', + '高級そうな欲望のつぼ': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'rumi', + name: 'ルミ', + giftRatings: { + 'おしゃれなくし': 'ss', + '可愛い食器セット': 'na', + '刺繍付きのハンカチ': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'rei', + name: 'レイ', + giftRatings: { + 'エイ~ブックレア': 'ss', + 'エーポッドプロ': 'nb', + 'ゲームガールカラー復刻版': 'nb', + '埋蔵金の地図': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'reisa', + name: 'レイサ', + giftRatings: { + 'ミルフィーユの正統派パフェ': 'ss', + 'エイ~ブックレア': 'sa', + 'エーポッドプロ': 'na', + '高級なクッキーセット': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '大きなホールケーキ': 'nb', + 'ぜんまい式オルゴール': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'reisa_magical', + name: 'レイサ(マジカル)', + giftRatings: { + 'ストリートオブヤンキー1巻': 'ss', + 'ミルフィーユの正統派パフェ': 'sa', + 'ボードゲーム「ザ・人生」': 'sa', + 'ゲームマガジン「ヒットガールズ」': 'na', + '禁断の愛~許されないからこそ美しく~': 'nb', + '高級なクッキーセット': 'nb', + '『銃 可愛い 青春』': 'nb', + '天体望遠鏡': 'nb', + 'ゲームガールカラー復刻版': 'nb', + 'MX-レーションC型デザート風味': 'nb', + '抹茶味の瓶ラムネ': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + 'O-フィット': 'nb', + 'コスプレ用ぐるぐるメガネ': 'nb', + 'リボンのついた熊のぬいぐるみ': 'nb', + '30色の絵の具セット': 'nb', + '古典の詩集': 'nb', + '頭脳開発キューブパズル': 'nb', + '大きなホールケーキ': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'reijo', + name: 'レイジョ', + giftRatings: { + '1日3回ダンベルセット': 'ss', + 'ザ・サプリメント': 'na', + 'ペロロの腹筋ローラー': 'nb', + 'O-フィット': 'nb', + '埋蔵金の地図': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'rena', + name: 'レナ', + giftRatings: { + '裁縫キット': 'ss', + 'サミュエラ「ザ・ビヨンド」': 'sa', + 'おしゃれなくし': 'sa', + 'リボンのついた熊のぬいぐるみ': 'na', + '刺繍付きのハンカチ': 'na', + 'チェリーローズカラーのグロス': 'nb', + 'お肌を透明にするBBクリーム': 'nb', + 'ミリタリー用カモフラージュクリーム3種セット': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'renge', + name: 'レンゲ', + giftRatings: { + '裁縫キット': 'ss', + '音楽演奏会入場券': 'sa', + '禁断の愛~許されないからこそ美しく~': 'na', + '映画の前売りペアチケット': 'na', + '可愛い食器セット': 'na', + 'ハイクラスビュッフェ招待券': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'renge_swimsuit', + name: 'レンゲ(水着)', + giftRatings: { + '裁縫キット': 'ss', + 'ストリートオブヤンキー1巻': 'sa', + '禁断の愛~許されないからこそ美しく~': 'na', + '夏模様の浮き輪': 'na', + 'ゲームマガジン「ヒットガールズ」': 'nb', + '『銃 可愛い 青春』': 'nb', + '跳躍探偵ウサギ~霧に包まれた温泉での滑落~': 'nb', + '古典の詩集': 'nb', + '可愛い食器セット': 'nb', + '百科事典': 'nb', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'wakamo', + name: 'ワカモ', + giftRatings: { + 'おしゃれなくし': 'ss', + 'リボンのついた熊のぬいぐるみ': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'wakamo_swimsuit', + name: 'ワカモ(水着)', + giftRatings: { + 'おしゃれなくし': 'ss', + 'リボンのついた熊のぬいぐるみ': 'na', + '夏模様の浮き輪': 'na', + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'hatsune_miku', + name: '初音ミク', + giftRatings: { + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'misaka_mikoto', + name: '御坂美琴', + giftRatings: { + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'shokuhou_misaki', + name: '食蜂操祈', + giftRatings: { + } as Record<string, keyof typeof GIFT_EXP> + }, + { + id: 'saten_ruiko', + name: '佐天涙子', + giftRatings: { + } as Record<string, keyof typeof GIFT_EXP> + }, +] as const; + +export type CharacterId = typeof CHARACTER_LIST[number]['id']; diff --git a/wiki2convert.py b/wiki2convert.py @@ -0,0 +1,64 @@ +import re + +# 1. ここにWikiのテーブル行を貼り付けてください +wiki_data = """ + +""" + +def generate_ts_file(raw_text, filename="converted_characters.txt"): + # 列構成の定義 (インデックス, 終了インデックス, レーティング) + # 0列目は名前なので除外、1列目からカウント + col_definitions = [ + (1, 2, 'ss'), # 高級贈り物 特大 (2列) + (3, 4, 'sa'), # 高級贈り物 大 (2列) + (5, 7, 'na'), # 通常贈り物 大 (3列) + (8, 15, 'nb') # 通常贈り物 中 (8列) + ] + + lines = raw_text.strip().split('\n') + formatted_output = [] + + for line in lines: + if not line.strip() or line.startswith('|~') or 'h' in line.split('|')[-1]: + continue + + # セルに分割 + cells = [c.strip() for c in line.strip('|').split('|')] + + # キャラクター名の抽出 + name_match = re.search(r'\[\[([^\]]+)\]\]$', cells[0]) + name = name_match.group(1) if name_match else "unknown" + + # IDの生成 (適宜調整してください) + char_id = name.lower().replace('(', '_').replace(')', '') + + # ギフトレーティングの解析 + gift_ratings = [] + for start, end, rating in col_definitions: + for i in range(start, end + 1): + if i < len(cells) and cells[i]: + # アイテム名の抽出 + item_name = re.search(r',([^,)]+)\);', cells[i]) + if item_name: + gift_ratings.append(f" '{item_name.group(1)}': '{rating}',") + + # TypeScript形式の文字列組み立て + char_block = [ + f" {{", + f" id: '{char_id}',", + f" name: '{name}',", + f" giftRatings: {{", + *gift_ratings, + f" }} as Record<string, keyof typeof GIFT_EXP>", + f" }}," + ] + formatted_output.append("\n".join(char_block)) + + # ファイル書き出し + with open(filename, "w", encoding="utf-8") as f: + f.write("\n".join(formatted_output)) + + print(f"成功: {filename} に書き出しました。") + +if __name__ == "__main__": + generate_ts_file(wiki_data)+ \ No newline at end of file