[html]
<style>
#boston-tamagotchi {
--bg: #17191f;
--panel: #20242d;
--panel2: #14171d;
--border: #343a45;
--text: #eeeeee;
--muted: #979ca6;
--accent: #9f2734;
width: 100%;
max-width: 620px;
margin: 25px auto;
padding: 22px;
box-sizing: border-box;
border-radius: 15px;
background: var(--bg);
color: var(--text);
font-family:
Arial,
sans-serif;
}
#boston-tamagotchi *,
#boston-tamagotchi *::before,
#boston-tamagotchi *::after {
box-sizing: border-box;
}
/* HEADER */
#boston-tamagotchi .tama-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 15px;
margin-bottom: 18px;
}
#boston-tamagotchi .tama-title {
font-size: 18px;
font-weight: bold;
}
#boston-tamagotchi .tama-online {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
color: var(--muted);
}
#boston-tamagotchi .tama-online-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #777;
}
#boston-tamagotchi.online
.tama-online-dot {
background: #65b96b;
}
/* ÏÈÒÎÌÅÖ */
#boston-tamagotchi .tama-pet {
display: flex;
gap: 22px;
align-items: center;
padding: 18px;
background: var(--panel);
border-radius: 11px;
}
#boston-tamagotchi .tama-creature {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 130px;
width: 130px;
height: 130px;
border-radius: 50%;
background: #eee;
font-size: 72px;
box-shadow:
inset 0 0 0 5px
rgba(0,0,0,.08);
}
/* ÑÒÀÒÛ */
#boston-tamagotchi .tama-stats {
flex: 1;
}
#boston-tamagotchi .tama-stat {
margin-bottom: 10px;
}
#boston-tamagotchi .tama-stat:last-child {
margin-bottom: 0;
}
#boston-tamagotchi .tama-stat-top {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
font-size: 10px;
color: #bbb;
}
#boston-tamagotchi .tama-bar {
height: 8px;
overflow: hidden;
background: #101217;
border-radius: 6px;
}
#boston-tamagotchi .tama-bar-fill {
width: 0;
height: 100%;
border-radius: 6px;
background: #ddd;
transition:
width .35s ease;
}
/* ÏÎËÜÇÎÂÀÒÅËÜ */
#boston-tamagotchi .tama-user {
margin-top: 15px;
padding: 12px 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
background: var(--panel2);
border: 1px solid var(--border);
border-radius: 8px;
}
#boston-tamagotchi .tama-user-name {
color: #bbb;
font-size: 11px;
}
#boston-tamagotchi .tama-wallet {
font-size: 11px;
color: #bbb;
}
#boston-tamagotchi .tama-wallet strong {
color: #fff;
font-size: 16px;
}
/* ÊÍÎÏÊÈ */
#boston-tamagotchi .tama-actions {
display: grid;
grid-template-columns:
repeat(2, 1fr);
gap: 8px;
margin-top: 15px;
}
#boston-tamagotchi
.tama-action {
min-height: 43px;
padding: 8px 12px;
border: 0;
border-radius: 7px;
background: #eceae5;
color: #181a20;
font-family: inherit;
font-size: 11px;
font-weight: bold;
cursor: pointer;
transition:
transform .1s ease,
filter .1s ease;
}
#boston-tamagotchi
.tama-action:hover {
filter: brightness(1.08);
}
#boston-tamagotchi
.tama-action:active {
transform: scale(.98);
}
#boston-tamagotchi
.tama-action:disabled {
opacity: .35;
cursor: default;
}
/* ÐÅÇÓËÜÒÀÒ */
#boston-tamagotchi .tama-message {
min-height: 38px;
margin-top: 12px;
padding: 10px 12px;
border-radius: 7px;
background: #111419;
color: #c5c7cb;
font-size: 11px;
line-height: 1.45;
}
/* ÆÓÐÍÀË */
#boston-tamagotchi .tama-log {
margin-top: 17px;
padding-top: 15px;
border-top:
1px solid var(--border);
}
#boston-tamagotchi .tama-log-title {
margin-bottom: 8px;
font-size: 9px;
font-weight: bold;
letter-spacing: 1px;
color: #777;
text-transform: uppercase;
}
#boston-tamagotchi .tama-log-item {
padding: 6px 0;
border-bottom:
1px solid rgba(255,255,255,.05);
font-size: 10px;
color: #aaa;
line-height: 1.4;
}
#boston-tamagotchi .tama-log-item strong {
color: #ddd;
}
/* ÌÎÁÈËÜÍÀß ÂÅÐÑÈß */
@media (max-width: 500px) {
#boston-tamagotchi {
padding: 15px;
}
#boston-tamagotchi .tama-pet {
flex-direction: column;
}
#boston-tamagotchi .tama-creature {
flex-basis: 110px;
width: 110px;
height: 110px;
font-size: 60px;
}
#boston-tamagotchi .tama-stats {
width: 100%;
}
}
</style>
<div id="boston-tamagotchi">
<div class="tama-header">
<div class="tama-title">
Íåèçâåñòíîå ñóùåñòâî
</div>
<div class="tama-online">
<span
class="tama-online-dot"
></span>
<span
id="tama-connection"
>
ïîäêëþ÷åíèå...
</span>
</div>
</div>
<div class="tama-pet">
<div class="tama-creature">
👻
</div>
<div class="tama-stats">
<div class="tama-stat">
<div class="tama-stat-top">
<span>
Ñûòîñòü
</span>
<span
id="tama-hunger-text"
>
—
</span>
</div>
<div class="tama-bar">
<div
class="tama-bar-fill"
id="tama-hunger"
></div>
</div>
</div>
<div class="tama-stat">
<div class="tama-stat-top">
<span>
Íàñòðîåíèå
</span>
<span
id="tama-mood-text"
>
—
</span>
</div>
<div class="tama-bar">
<div
class="tama-bar-fill"
id="tama-mood"
></div>
</div>
</div>
<div class="tama-stat">
<div class="tama-stat-top">
<span>
Ýíåðãèÿ
</span>
<span
id="tama-energy-text"
>
—
</span>
</div>
<div class="tama-bar">
<div
class="tama-bar-fill"
id="tama-energy"
></div>
</div>
</div>
<div class="tama-stat">
<div class="tama-stat-top">
<span>
Õàîñ
</span>
<span
id="tama-chaos-text"
>
—
</span>
</div>
<div class="tama-bar">
<div
class="tama-bar-fill"
id="tama-chaos"
></div>
</div>
</div>
</div>
</div>
<div class="tama-user">
<div
class="tama-user-name"
id="tama-user-name"
>
Îïðåäåëÿåì ïîëüçîâàòåëÿ...
</div>
<div class="tama-wallet">
Äåéñòâèé:
<strong
id="tama-balance"
>
—
</strong>
🐾
</div>
</div>
<div class="tama-actions">
<button
class="tama-action"
type="button"
data-action="feed"
>
🍖 Ïîêîðìèòü
</button>
<button
class="tama-action"
type="button"
data-action="play"
>
🎾 Ïîèãðàòü
</button>
<button
class="tama-action"
type="button"
data-action="pet"
>
❤️ Ïîãëàäèòü
</button>
<button
class="tama-action"
type="button"
data-action="sleep"
>
💤 Óëîæèòü ñïàòü
</button>
</div>
<div
class="tama-message"
id="tama-message"
>
Êàæäûé èãðîâîé ïîñò äà¸ò îäíî äåéñòâèå.
</div>
<div class="tama-log">
<div class="tama-log-title">
×òî ïðîèñõîäèëî
</div>
<div
id="tama-log-list"
></div>
</div>
</div>
<script>
(function () {
/* ===================================================
ÍÀÑÒÐÎÉÊÈ
=================================================== */
var DATABASE_URL =
'https://boston-board-game-default-rtdb.europe-west1.firebasedatabase.app';
/*
Ñòàðòîâûå õàðàêòåðèñòèêè.
*/
var BASE_STATS = {
hunger: 70,
mood: 65,
energy: 65,
chaos: 20
};
/*
Âëèÿíèå äåéñòâèé.
*/
var ACTIONS = {
feed: {
label:
'ïîêîðìèë ñóùåñòâî',
result:
'Ñóùåñòâî äîâîëüíî ñúåëî ïðåäëîæåííîå. Êàæåòñÿ, âìåñòå ñ òàðåëêîé.',
hunger:
12,
mood:
1,
energy:
0,
chaos:
1
},
play: {
label:
'ïîèãðàë ñ ñóùåñòâîì',
result:
'Âû ïîèãðàëè. Òåïåðü êòî-òî äîëæåí îáúÿñíèòü, ïî÷åìó ñòóë íàõîäèòñÿ íà ïîòîëêå.',
hunger:
-3,
mood:
10,
energy:
-5,
chaos:
5
},
pet: {
label:
'ïîãëàäèë ñóùåñòâî',
result:
'Îíî ïîçâîëèëî ñåáÿ ïîãëàäèòü. Ýòî ïîäîçðèòåëüíî.',
hunger:
0,
mood:
7,
energy:
1,
chaos:
-2
},
sleep: {
label:
'óëîæèë ñóùåñòâî ñïàòü',
result:
'Ñóùåñòâî ñâåðíóëîñü êëóáêîì. Èëè ýòî ïðîñòî î÷åíü ñòðàííàÿ ïîçà.',
hunger:
-2,
mood:
1,
energy:
12,
chaos:
-2
}
};
/* ===================================================
ÝËÅÌÅÍÒÛ
=================================================== */
var root =
document.getElementById(
'boston-tamagotchi'
);
if (!root) {
return;
}
var connection =
document.getElementById(
'tama-connection'
);
var userNameElement =
document.getElementById(
'tama-user-name'
);
var balanceElement =
document.getElementById(
'tama-balance'
);
var messageElement =
document.getElementById(
'tama-message'
);
var logElement =
document.getElementById(
'tama-log-list'
);
var buttons =
root.querySelectorAll(
'.tama-action'
);
/* ===================================================
ÑÎÑÒÎßÍÈÅ
=================================================== */
var currentUser = null;
var databaseData = {};
var busy = false;
var stream = null;
/* ===================================================
URL
=================================================== */
function tamaUrl() {
return (
DATABASE_URL +
'/tamagotchi.json'
);
}
function actionUrl(
userId,
postId
) {
return (
DATABASE_URL +
'/tamagotchi/actions/' +
encodeURIComponent(
userId
) +
'/' +
encodeURIComponent(
postId
) +
'.json'
);
}
/* ===================================================
ÎÁÙÅÍÈÅ Ñ ÐÎÄÈÒÅËÜÑÊÈÌ ÔÎÐÓÌÎÌ
=================================================== */
function askForUser() {
try {
window.parent.postMessage(
{
type:
'BOSTON_TAMA_HELLO'
},
'*'
);
} catch (error) {}
}
window.addEventListener(
'message',
function (event) {
if (
!event.data ||
event.data.type !==
'BOSTON_TAMA_USER'
) {
return;
}
if (
event.data.loggedIn
) {
currentUser = {
id:
String(
event.data.userId
),
name:
String(
event.data.userName
)
};
userNameElement.textContent =
currentUser.name;
} else {
currentUser =
null;
userNameElement.textContent =
'Ãîñòü';
messageElement.textContent =
'×òîáû óõàæèâàòü çà ñóùåñòâîì, íåîáõîäèìî âîéòè íà ôîðóì.';
}
render();
}
);
askForUser();
var userTimer =
setInterval(
function () {
if (currentUser) {
clearInterval(
userTimer
);
return;
}
askForUser();
},
1000
);
setTimeout(
function () {
clearInterval(
userTimer
);
},
10000
);
/* ===================================================
ÇÀÃÐÓÇÊÀ FIREBASE
=================================================== */
async function loadData() {
try {
var response =
await fetch(
tamaUrl(),
{
cache:
'no-store'
}
);
if (!response.ok) {
throw new Error(
'HTTP ' +
response.status
);
}
databaseData =
await response.json() ||
{};
root.classList.add(
'online'
);
connection.textContent =
'îíëàéí';
render();
} catch (error) {
root.classList.remove(
'online'
);
connection.textContent =
'îøèáêà ñîåäèíåíèÿ';
console.error(
'Tamagotchi:',
error
);
}
}
/* ===================================================
ÂÑÅ ÄÅÉÑÒÂÈß
=================================================== */
function getAllActions() {
var result = [];
var actions =
databaseData.actions ||
{};
Object.keys(
actions
).forEach(
function (userId) {
var userActions =
actions[userId] ||
{};
Object.keys(
userActions
).forEach(
function (postId) {
var action =
userActions[
postId
];
if (!action) {
return;
}
result.push(
{
userId:
userId,
postId:
postId,
type:
action.type,
userName:
action.userName ||
'Êòî-òî',
createdAt:
Number(
action.createdAt
) || 0
}
);
}
);
}
);
result.sort(
function (a, b) {
return (
b.createdAt -
a.createdAt
);
}
);
return result;
}
/* ===================================================
ÍÅÈÑÏÎËÜÇÎÂÀÍÍÛÅ ÏÎÑÒÛ
=================================================== */
function getUnusedCredits() {
if (!currentUser) {
return [];
}
var credits =
(
databaseData.credits &&
databaseData.credits[
currentUser.id
]
) || {};
var actions =
(
databaseData.actions &&
databaseData.actions[
currentUser.id
]
) || {};
return Object.keys(
credits
)
.filter(
function (postId) {
return !actions[
postId
];
}
)
.sort(
function (a, b) {
return (
Number(a) -
Number(b)
);
}
);
}
/* ===================================================
ÕÀÐÀÊÒÅÐÈÑÒÈÊÈ ÏÈÒÎÌÖÀ
=================================================== */
function clamp(value) {
return Math.max(
0,
Math.min(
100,
Math.round(
value
)
)
);
}
function calculateStats() {
var stats = {
hunger:
BASE_STATS.hunger,
mood:
BASE_STATS.mood,
energy:
BASE_STATS.energy,
chaos:
BASE_STATS.chaos
};
var actions =
getAllActions();
/*
Ìåäëåííîå åñòåñòâåííîå
óõóäøåíèå ïîêàçàòåëåé.
Îòñ÷¸ò íà÷èíàåòñÿ
ñ ïåðâîãî äåéñòâèÿ.
*/
if (actions.length) {
var oldest =
actions[
actions.length - 1
].createdAt;
var hours =
Math.max(
0,
(
Date.now() -
oldest
) /
3600000
);
stats.hunger -=
Math.floor(
hours / 6
) * 2;
stats.mood -=
Math.floor(
hours / 12
);
stats.energy -=
Math.floor(
hours / 8
) * 2;
stats.chaos -=
Math.floor(
hours / 24
);
}
actions.forEach(
function (action) {
var effect =
ACTIONS[
action.type
];
if (!effect) {
return;
}
stats.hunger +=
effect.hunger;
stats.mood +=
effect.mood;
stats.energy +=
effect.energy;
stats.chaos +=
effect.chaos;
}
);
stats.hunger =
clamp(
stats.hunger
);
stats.mood =
clamp(
stats.mood
);
stats.energy =
clamp(
stats.energy
);
stats.chaos =
clamp(
stats.chaos
);
return stats;
}
/* ===================================================
ÏÎËÎÑÊÀ
=================================================== */
function setBar(
name,
value
) {
var bar =
document.getElementById(
'tama-' + name
);
var text =
document.getElementById(
'tama-' +
name +
'-text'
);
if (bar) {
bar.style.width =
value + '%';
}
if (text) {
text.textContent =
value + '%';
}
}
/* ===================================================
ÆÓÐÍÀË
=================================================== */
function renderLog() {
var actions =
getAllActions()
.slice(
0,
8
);
logElement.innerHTML = '';
if (!actions.length) {
var empty =
document.createElement(
'div'
);
empty.className =
'tama-log-item';
empty.textContent =
'Ïîêà íè÷åãî íå ïðîèñõîäèëî. Ñóùåñòâî íàáëþäàåò.';
logElement.appendChild(
empty
);
return;
}
actions.forEach(
function (action) {
var effect =
ACTIONS[
action.type
];
if (!effect) {
return;
}
var item =
document.createElement(
'div'
);
item.className =
'tama-log-item';
var strong =
document.createElement(
'strong'
);
strong.textContent =
action.userName;
var text =
document.createTextNode(
' ' +
effect.label +
'.'
);
item.appendChild(
strong
);
item.appendChild(
text
);
logElement.appendChild(
item
);
}
);
}
/* ===================================================
ÎÒÎÁÐÀÆÅÍÈÅ
=================================================== */
function render() {
var stats =
calculateStats();
setBar(
'hunger',
stats.hunger
);
setBar(
'mood',
stats.mood
);
setBar(
'energy',
stats.energy
);
setBar(
'chaos',
stats.chaos
);
var balance =
getUnusedCredits()
.length;
balanceElement.textContent =
currentUser
? balance
: '—';
Array.prototype.forEach.call(
buttons,
function (button) {
button.disabled =
(
busy ||
!currentUser ||
balance <= 0
);
}
);
renderLog();
}
/* ===================================================
ÄÅÉÑÒÂÈÅ
=================================================== */
async function performAction(
type
) {
if (
busy ||
!currentUser ||
!ACTIONS[type]
) {
return;
}
busy = true;
render();
/*
Ïåðåä òðàòîé îáíîâëÿåì áàçó,
÷òîáû íå èñïîëüçîâàòü
óæå ïîòðà÷åííûé ïîñò.
*/
await loadData();
var unused =
getUnusedCredits();
if (!unused.length) {
messageElement.textContent =
'Ó âàñ íåò ñâîáîäíûõ äåéñòâèé. Íàïèøèòå èãðîâîé ïîñò — îí äàñò +1 🐾.';
busy = false;
render();
return;
}
/*
Áåð¸ì ñàìûé ñòàðûé
íåèñïîëüçîâàííûé èãðîâîé ïîñò.
*/
var postId =
unused[0];
try {
var response =
await fetch(
actionUrl(
currentUser.id,
postId
),
{
method:
'PUT',
headers: {
'Content-Type':
'application/json'
},
body:
JSON.stringify(
{
type:
type,
userName:
currentUser.name,
createdAt:
Date.now()
}
)
}
);
if (!response.ok) {
throw new Error(
'HTTP ' +
response.status
);
}
messageElement.textContent =
ACTIONS[type]
.result;
await loadData();
} catch (error) {
console.error(
'Tamagotchi action:',
error
);
messageElement.textContent =
'Íå óäàëîñü âûïîëíèòü äåéñòâèå. Âîçìîæíî, ýòîò æåòîí óæå áûë ïîòðà÷åí â äðóãîé âêëàäêå.';
await loadData();
}
busy = false;
render();
}
/* ===================================================
ÊÍÎÏÊÈ
=================================================== */
Array.prototype.forEach.call(
buttons,
function (button) {
button.addEventListener(
'click',
function () {
performAction(
this.getAttribute(
'data-action'
)
);
}
);
}
);
/* ===================================================
REALTIME
=================================================== */
function startRealtime() {
try {
stream =
new EventSource(
tamaUrl()
);
stream.addEventListener(
'open',
function () {
root.classList.add(
'online'
);
connection.textContent =
'îíëàéí';
}
);
stream.addEventListener(
'put',
function () {
loadData();
}
);
stream.addEventListener(
'patch',
function () {
loadData();
}
);
stream.onerror =
function () {
connection.textContent =
'ïåðåïîäêëþ÷åíèå...';
};
} catch (error) {
console.error(
error
);
}
}
/*
Äàæå åñëè EventSource âíóòðè
Safe HTML îêàæåòñÿ çàáëîêèðîâàí,
ðàç â 10 ñåêóíä âñ¸ ðàâíî
îáíîâëÿåì ñîñòîÿíèå.
*/
setInterval(
loadData,
10000
);
/* ===================================================
ÇÀÏÓÑÊ
=================================================== */
loadData();
startRealtime();
})();
</script>
[/html]
- Ïîäïèñü àâòîðà
















































