jntzjn
This commit is contained in:
parent
b486199ca2
commit
109842b8de
2
app.js
2
app.js
@ -33,6 +33,7 @@ const combineRoutes = require("./routes/combine.route");
|
||||
const bazaarRoutes = require("./routes/bazaar.route");
|
||||
const himmelstorRoutes = require("./routes/himmelstor.route");
|
||||
const himmelstorDailyRoutes = require("./routes/himmelstor-daily.route");
|
||||
const gildenhalleRoutes = require("./routes/gildenhalle.route");
|
||||
|
||||
const compression = require("compression");
|
||||
|
||||
@ -433,6 +434,7 @@ app.use("/api", combineRoutes);
|
||||
app.use("/api", bazaarRoutes);
|
||||
app.use("/himmelstor", himmelstorRoutes);
|
||||
app.use("/api/himmelstor/daily", himmelstorDailyRoutes);
|
||||
app.use("/api", gildenhalleRoutes);
|
||||
|
||||
/* ========================
|
||||
Energie abfragen
|
||||
|
||||
522
public/css/gildenhalle.css
Normal file
522
public/css/gildenhalle.css
Normal file
@ -0,0 +1,522 @@
|
||||
/* ============================================================
|
||||
public/css/gildenhalle.css
|
||||
============================================================ */
|
||||
|
||||
/* ── Gilden-spezifische Farben ───────────────────────────── */
|
||||
:root {
|
||||
--guild-gold: #f0d060;
|
||||
--guild-border: #8b6a3c;
|
||||
--guild-dark: #1a0f04;
|
||||
--guild-mid: #2a1a08;
|
||||
--guild-text: #f0d9a6;
|
||||
--guild-muted: #a08060;
|
||||
--guild-green: #4aaa30;
|
||||
--guild-red: #cc3030;
|
||||
}
|
||||
|
||||
/* ── Mitglieder-Tabelle ─────────────────────────────────── */
|
||||
.gh-members-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
.gh-members-table th {
|
||||
color: var(--guild-muted);
|
||||
font-size: 10px;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--guild-border);
|
||||
text-align: left;
|
||||
}
|
||||
.gh-members-table td {
|
||||
padding: 7px 10px;
|
||||
border-bottom: 1px solid rgba(139,106,42,.2);
|
||||
color: var(--guild-text);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.gh-members-table tr:hover td {
|
||||
background: rgba(255,255,255,.03);
|
||||
}
|
||||
|
||||
/* ── Rang-Badge ─────────────────────────────────────────── */
|
||||
.gh-rank-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(#3a2810, #1a0f04);
|
||||
border: 1px solid var(--guild-border);
|
||||
color: var(--guild-gold);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.gh-rank-badge.leader {
|
||||
border-color: var(--guild-gold);
|
||||
background: linear-gradient(#6b4b2a, #3c2414);
|
||||
box-shadow: 0 0 6px rgba(200,160,60,.3);
|
||||
}
|
||||
|
||||
/* ── Gilden-Karten im Suchraster ────────────────────────── */
|
||||
.gh-guild-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
overflow-y: auto;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.gh-guild-card {
|
||||
background: linear-gradient(135deg, var(--guild-mid), var(--guild-dark));
|
||||
border: 2px solid var(--guild-border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
transition: .2s;
|
||||
cursor: default;
|
||||
}
|
||||
.gh-guild-card:hover {
|
||||
border-color: var(--guild-gold);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,.5), 0 0 10px rgba(200,160,60,.15);
|
||||
}
|
||||
|
||||
.gh-guild-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.gh-guild-tag {
|
||||
background: linear-gradient(#6b4b2a, #3c2414);
|
||||
border: 1px solid var(--guild-gold);
|
||||
border-radius: 6px;
|
||||
padding: 2px 8px;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: var(--guild-gold);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.gh-guild-name {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: var(--guild-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.gh-guild-desc {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 10px;
|
||||
color: var(--guild-muted);
|
||||
line-height: 1.5;
|
||||
flex: 1;
|
||||
}
|
||||
.gh-guild-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 10px;
|
||||
color: var(--guild-muted);
|
||||
}
|
||||
.gh-guild-open {
|
||||
color: var(--guild-green);
|
||||
font-weight: bold;
|
||||
}
|
||||
.gh-guild-closed {
|
||||
color: #e0a030;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.gh-join-btn {
|
||||
margin-top: 4px;
|
||||
padding: 5px 0;
|
||||
background: linear-gradient(#3a2810, #1a0f04);
|
||||
border: 2px solid var(--guild-border);
|
||||
border-radius: 6px;
|
||||
color: var(--guild-text);
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: .15s;
|
||||
width: 100%;
|
||||
}
|
||||
.gh-join-btn:hover:not(:disabled) {
|
||||
border-color: var(--guild-gold);
|
||||
color: #fff;
|
||||
}
|
||||
.gh-join-btn:disabled {
|
||||
opacity: .45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.gh-join-btn.pending {
|
||||
border-color: #e0a030;
|
||||
color: #e0a030;
|
||||
}
|
||||
|
||||
/* ── Gilde gründen ──────────────────────────────────────── */
|
||||
.gh-create-form {
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-width: 440px;
|
||||
}
|
||||
.gh-form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.gh-form-label {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
color: var(--guild-muted);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.gh-form-input,
|
||||
.gh-form-textarea,
|
||||
.gh-form-select {
|
||||
background: rgba(0,0,0,.5);
|
||||
border: 2px solid var(--guild-border);
|
||||
border-radius: 7px;
|
||||
color: var(--guild-text);
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 13px;
|
||||
padding: 7px 10px;
|
||||
outline: none;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.gh-form-input:focus,
|
||||
.gh-form-textarea:focus,
|
||||
.gh-form-select:focus { border-color: var(--guild-gold); }
|
||||
.gh-form-textarea { resize: vertical; min-height: 70px; }
|
||||
.gh-form-select { cursor: pointer; }
|
||||
|
||||
.gh-create-btn {
|
||||
padding: 10px 0;
|
||||
background: linear-gradient(#6b4b2a, #3c2414);
|
||||
border: 2px solid var(--guild-gold);
|
||||
border-radius: 8px;
|
||||
color: #fff5cc;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: .2s;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.gh-create-btn:hover {
|
||||
box-shadow: 0 0 14px rgba(200,160,60,.4);
|
||||
}
|
||||
|
||||
.gh-error-msg {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
color: #ff6666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Aufgaben ────────────────────────────────────────────── */
|
||||
.gh-tasks-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.gh-task-card {
|
||||
background: linear-gradient(135deg, var(--guild-mid), var(--guild-dark));
|
||||
border: 2px solid var(--guild-border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
transition: .2s;
|
||||
}
|
||||
.gh-task-card.completed {
|
||||
border-color: var(--guild-green);
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.gh-task-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.gh-task-label {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 13px;
|
||||
color: var(--guild-text);
|
||||
font-weight: bold;
|
||||
}
|
||||
.gh-task-reward {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
color: var(--guild-gold);
|
||||
}
|
||||
.gh-task-done-badge {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
color: var(--guild-green);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.gh-task-progress-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.gh-task-bar-track {
|
||||
height: 8px;
|
||||
background: rgba(255,255,255,.08);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(139,106,42,.3);
|
||||
}
|
||||
.gh-task-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4a8a3c, #7ada60);
|
||||
border-radius: 4px;
|
||||
transition: width .5s ease;
|
||||
}
|
||||
.gh-task-bar-fill.completed {
|
||||
background: linear-gradient(90deg, var(--guild-green), #8aff60);
|
||||
}
|
||||
.gh-task-progress-text {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 10px;
|
||||
color: var(--guild-muted);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* ── Anfragen-Liste ─────────────────────────────────────── */
|
||||
.gh-requests-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.gh-request-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0,0,0,.25);
|
||||
border: 1px solid rgba(139,106,42,.25);
|
||||
border-radius: 7px;
|
||||
gap: 10px;
|
||||
}
|
||||
.gh-request-name {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 12px;
|
||||
color: var(--guild-text);
|
||||
flex: 1;
|
||||
}
|
||||
.gh-request-msg {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 10px;
|
||||
color: var(--guild-muted);
|
||||
font-style: italic;
|
||||
flex: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.gh-req-accept,
|
||||
.gh-req-reject {
|
||||
padding: 4px 12px;
|
||||
border-radius: 5px;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: .15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.gh-req-accept {
|
||||
background: linear-gradient(#1a4a18, #0f2a0e);
|
||||
border: 2px solid #4a8a3c;
|
||||
color: #a0e090;
|
||||
}
|
||||
.gh-req-accept:hover { border-color: #7ada60; }
|
||||
.gh-req-reject {
|
||||
background: linear-gradient(#4a1010, #2a0808);
|
||||
border: 2px solid #8a3030;
|
||||
color: #e07070;
|
||||
}
|
||||
.gh-req-reject:hover { border-color: #ff6060; }
|
||||
|
||||
/* ── Meine Gilde Info-Box ───────────────────────────────── */
|
||||
.gh-my-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(0,0,0,.3);
|
||||
border-bottom: 1px solid var(--guild-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.gh-my-tag {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--guild-gold);
|
||||
background: linear-gradient(#6b4b2a, #3c2414);
|
||||
border: 2px solid var(--guild-gold);
|
||||
border-radius: 8px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
.gh-my-name {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: var(--guild-text);
|
||||
}
|
||||
.gh-my-sub {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
color: var(--guild-muted);
|
||||
}
|
||||
.gh-leave-btn {
|
||||
margin-left: auto;
|
||||
padding: 6px 16px;
|
||||
background: linear-gradient(#3a1010, #1a0808);
|
||||
border: 2px solid #6a2020;
|
||||
border-radius: 7px;
|
||||
color: #d08080;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: .15s;
|
||||
}
|
||||
.gh-leave-btn:hover { border-color: #cc4040; color: #ff9090; }
|
||||
|
||||
/* ── Suche ──────────────────────────────────────────────── */
|
||||
.gh-search-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid rgba(139,106,42,.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.gh-search-input {
|
||||
flex: 1;
|
||||
background: rgba(0,0,0,.5);
|
||||
border: 2px solid var(--guild-border);
|
||||
border-radius: 7px;
|
||||
color: var(--guild-text);
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 12px;
|
||||
padding: 6px 10px;
|
||||
outline: none;
|
||||
}
|
||||
.gh-search-input:focus { border-color: var(--guild-gold); }
|
||||
.gh-search-btn {
|
||||
padding: 6px 16px;
|
||||
background: linear-gradient(#3a2810, #1a0f04);
|
||||
border: 2px solid var(--guild-border);
|
||||
border-radius: 7px;
|
||||
color: var(--guild-text);
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: .15s;
|
||||
}
|
||||
.gh-search-btn:hover { border-color: var(--guild-gold); }
|
||||
|
||||
/* ── Keine Gilde Hinweis ────────────────────────────────── */
|
||||
.gh-no-guild {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
font-family: "Cinzel", serif;
|
||||
color: var(--guild-muted);
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.gh-no-guild-icon { font-size: 42px; }
|
||||
.gh-no-guild-text { font-size: 13px; line-height: 1.8; }
|
||||
|
||||
/* ── Pagination ─────────────────────────────────────────── */
|
||||
.gh-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
padding: 8px;
|
||||
border-top: 1px solid rgba(139,106,42,.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.gh-page-btn {
|
||||
background: linear-gradient(#3a2810, #1a0f04);
|
||||
border: 1px solid var(--guild-border);
|
||||
border-radius: 5px;
|
||||
color: var(--guild-text);
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
padding: 3px 10px;
|
||||
cursor: pointer;
|
||||
transition: .15s;
|
||||
}
|
||||
.gh-page-btn:hover:not(:disabled) { border-color: var(--guild-gold); }
|
||||
.gh-page-btn:disabled { opacity: .35; cursor: not-allowed; }
|
||||
.gh-page-btn.active {
|
||||
background: linear-gradient(#6b4b2a, #3c2414);
|
||||
border-color: var(--guild-gold);
|
||||
}
|
||||
|
||||
/* ── Lade/Leer-Zustand ──────────────────────────────────── */
|
||||
.gh-loading, .gh-empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 13px;
|
||||
color: var(--guild-muted);
|
||||
}
|
||||
|
||||
/* ── Section-Header innerhalb Panel ────────────────────── */
|
||||
.gh-section-title {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 12px;
|
||||
color: var(--guild-gold);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
padding: 8px 16px 4px;
|
||||
border-bottom: 1px solid rgba(139,106,42,.25);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Rang-Select in Mitglieder-Tabelle ──────────────────── */
|
||||
.gh-rank-select {
|
||||
background: rgba(0,0,0,.5);
|
||||
border: 1px solid var(--guild-border);
|
||||
border-radius: 5px;
|
||||
color: var(--guild-text);
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
padding: 2px 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.gh-kick-btn {
|
||||
padding: 2px 8px;
|
||||
background: linear-gradient(#4a1010, #2a0808);
|
||||
border: 1px solid #8a3030;
|
||||
border-radius: 5px;
|
||||
color: #e07070;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
transition: .15s;
|
||||
}
|
||||
.gh-kick-btn:hover { border-color: #cc4040; }
|
||||
547
public/js/buildings/gildenhalle.js
Normal file
547
public/js/buildings/gildenhalle.js
Normal file
@ -0,0 +1,547 @@
|
||||
/* ============================================================
|
||||
public/js/buildings/gildenhalle.js
|
||||
============================================================ */
|
||||
|
||||
let gh_initialized = false;
|
||||
let gh_page = 1;
|
||||
let gh_search = '';
|
||||
let gh_playerGuild = null;
|
||||
|
||||
function ghLoadCSS() {
|
||||
if (!document.querySelector('link[href="/css/gildenhalle.css"]')) {
|
||||
const l = document.createElement('link');
|
||||
l.rel = 'stylesheet'; l.href = '/css/gildenhalle.css';
|
||||
document.head.appendChild(l);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Popup sicherstellen ────────────────────────────────── */
|
||||
function ghEnsurePopup() {
|
||||
if (document.getElementById('gildenhalle-popup')) return;
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.id = 'gildenhalle-popup';
|
||||
popup.className = 'qm-popup';
|
||||
popup.innerHTML = `
|
||||
<div class="qm-popup-header">
|
||||
<span class="qm-popup-title">⚔ Gildenhalle</span>
|
||||
<div class="baz-header-right">
|
||||
<span class="qm-popup-close" id="gh-close">✕</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mp-body-wrap">
|
||||
<aside class="mp-tabs" id="gh-tabs">
|
||||
<button class="mp-tab mp-tab-active" data-tab="gh-panel-1">
|
||||
<span class="mp-tab-dot"></span><span>Gilden</span>
|
||||
</button>
|
||||
<button class="mp-tab" data-tab="gh-panel-2">
|
||||
<span class="mp-tab-dot"></span><span>Eigene Gilde</span>
|
||||
</button>
|
||||
<button class="mp-tab" data-tab="gh-panel-3">
|
||||
<span class="mp-tab-dot"></span><span>Gilden Aufgaben</span>
|
||||
</button>
|
||||
</aside>
|
||||
<div class="mp-content">
|
||||
|
||||
<!-- ── PANEL 1: Gilden suchen ───────────────────── -->
|
||||
<div class="mp-panel active" id="gh-panel-1" style="overflow:hidden;">
|
||||
<div class="mp-col-header">Gilden</div>
|
||||
<div class="gh-search-bar">
|
||||
<input class="gh-search-input" id="gh-search-input"
|
||||
placeholder="Gildennamen oder Tag suchen…" />
|
||||
<button class="gh-search-btn" id="gh-search-btn">Suchen</button>
|
||||
<button class="gh-search-btn" id="gh-create-toggle-btn"
|
||||
style="border-color:#f0d060;color:#f0d060;">+ Gründen</button>
|
||||
</div>
|
||||
<div class="gh-guild-grid" id="gh-guild-grid">
|
||||
<div class="gh-loading">Lade Gilden…</div>
|
||||
</div>
|
||||
<div class="gh-pagination" id="gh-pagination"></div>
|
||||
|
||||
<!-- Gründen-Formular (versteckt) -->
|
||||
<div id="gh-create-form-wrap" style="display:none;position:absolute;inset:0;
|
||||
background:rgba(10,6,2,.96);z-index:20;overflow-y:auto;">
|
||||
<div class="gh-create-form">
|
||||
<div class="mp-col-header" style="margin-bottom:4px;">Neue Gilde gründen</div>
|
||||
<div class="gh-form-row">
|
||||
<label class="gh-form-label">Gildenname *</label>
|
||||
<input class="gh-form-input" id="gh-new-name" maxlength="50" placeholder="Name der Gilde">
|
||||
</div>
|
||||
<div class="gh-form-row">
|
||||
<label class="gh-form-label">Tag (2–6 Zeichen) *</label>
|
||||
<input class="gh-form-input" id="gh-new-tag" maxlength="6" placeholder="z.B. DOK"
|
||||
style="text-transform:uppercase;">
|
||||
</div>
|
||||
<div class="gh-form-row">
|
||||
<label class="gh-form-label">Beschreibung</label>
|
||||
<textarea class="gh-form-textarea" id="gh-new-desc" maxlength="255"
|
||||
placeholder="Kurze Beschreibung…"></textarea>
|
||||
</div>
|
||||
<div class="gh-form-row">
|
||||
<label class="gh-form-label">Beitrittsmodus</label>
|
||||
<select class="gh-form-select" id="gh-new-open">
|
||||
<option value="1">Offen – jeder kann beitreten</option>
|
||||
<option value="0">Geschlossen – nur per Anfrage</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="gh-error-msg" id="gh-create-error"></div>
|
||||
<button class="gh-create-btn" id="gh-create-submit">⚔ GILDE GRÜNDEN</button>
|
||||
<button class="gh-search-btn" id="gh-create-cancel"
|
||||
style="text-align:center;">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── PANEL 2: Eigene Gilde ─────────────────────── -->
|
||||
<div class="mp-panel" id="gh-panel-2" style="overflow:hidden;flex-direction:column;">
|
||||
<div class="mp-col-header">Eigene Gilde</div>
|
||||
<div id="gh-my-content" style="flex:1;overflow-y:auto;display:flex;flex-direction:column;">
|
||||
<div class="gh-loading">Lade…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── PANEL 3: Gilden Aufgaben ───────────────────── -->
|
||||
<div class="mp-panel" id="gh-panel-3" style="overflow:hidden;flex-direction:column;">
|
||||
<div class="mp-col-header">Gilden Aufgaben</div>
|
||||
<div id="gh-tasks-content" style="flex:1;overflow-y:auto;">
|
||||
<div class="gh-loading">Lade Aufgaben…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(popup);
|
||||
|
||||
popup.querySelector('#gh-close').addEventListener('click', ghClose);
|
||||
|
||||
/* Tab-Wechsel */
|
||||
popup.querySelectorAll('.mp-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
popup.querySelectorAll('.mp-tab').forEach(t => t.classList.remove('mp-tab-active'));
|
||||
popup.querySelectorAll('.mp-panel').forEach(p => p.classList.remove('active'));
|
||||
btn.classList.add('mp-tab-active');
|
||||
const panel = document.getElementById(btn.dataset.tab);
|
||||
panel?.classList.add('active');
|
||||
|
||||
if (btn.dataset.tab === 'gh-panel-2') ghLoadMyGuild();
|
||||
if (btn.dataset.tab === 'gh-panel-3') ghLoadTasks();
|
||||
});
|
||||
});
|
||||
|
||||
/* Gründen-Toggle */
|
||||
document.getElementById('gh-create-toggle-btn').addEventListener('click', () => {
|
||||
document.getElementById('gh-create-form-wrap').style.display = 'block';
|
||||
document.getElementById('gh-create-error').textContent = '';
|
||||
});
|
||||
document.getElementById('gh-create-cancel').addEventListener('click', () => {
|
||||
document.getElementById('gh-create-form-wrap').style.display = 'none';
|
||||
});
|
||||
document.getElementById('gh-create-submit').addEventListener('click', ghCreateGuild);
|
||||
|
||||
/* Suche */
|
||||
document.getElementById('gh-search-btn').addEventListener('click', () => {
|
||||
gh_search = document.getElementById('gh-search-input').value.trim();
|
||||
gh_page = 1;
|
||||
ghLoadGuildList();
|
||||
});
|
||||
document.getElementById('gh-search-input').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') document.getElementById('gh-search-btn').click();
|
||||
});
|
||||
|
||||
/* Drag */
|
||||
const header = popup.querySelector('.qm-popup-header');
|
||||
let dragging=false, sx,sy,sl,st;
|
||||
header.style.cursor='grab';
|
||||
header.addEventListener('mousedown', e => {
|
||||
if (e.target.classList.contains('qm-popup-close')) return;
|
||||
dragging=true; header.style.cursor='grabbing';
|
||||
const r=popup.getBoundingClientRect();
|
||||
sx=e.clientX; sy=e.clientY; sl=r.left; st=r.top;
|
||||
popup.style.transform='none';
|
||||
popup.style.left=sl+'px'; popup.style.top=st+'px';
|
||||
e.preventDefault();
|
||||
});
|
||||
document.addEventListener('mousemove', e => {
|
||||
if (!dragging) return;
|
||||
popup.style.left=(sl+(e.clientX-sx))+'px';
|
||||
popup.style.top=(st+(e.clientY-sy))+'px';
|
||||
});
|
||||
document.addEventListener('mouseup', () => { dragging=false; header.style.cursor='grab'; });
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
PANEL 1: Gilden-Liste laden
|
||||
════════════════════════════════════════════ */
|
||||
async function ghLoadGuildList() {
|
||||
const grid = document.getElementById('gh-guild-grid');
|
||||
const pagination = document.getElementById('gh-pagination');
|
||||
if (!grid) return;
|
||||
grid.innerHTML = '<div class="gh-loading">Lade Gilden…</div>';
|
||||
if (pagination) pagination.innerHTML = '';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/gildenhalle/list?search=${encodeURIComponent(gh_search)}&page=${gh_page}`);
|
||||
const data = await res.json();
|
||||
gh_playerGuild = data.playerGuildId;
|
||||
|
||||
if (!data.guilds.length) {
|
||||
grid.innerHTML = '<div class="gh-empty">Keine Gilden gefunden.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = data.guilds.map(g => `
|
||||
<div class="gh-guild-card">
|
||||
<div class="gh-guild-header">
|
||||
<span class="gh-guild-tag">[${g.tag}]</span>
|
||||
<span class="gh-guild-name">${g.name}</span>
|
||||
</div>
|
||||
<div class="gh-guild-desc">${g.description || '<em>Keine Beschreibung</em>'}</div>
|
||||
<div class="gh-guild-meta">
|
||||
<span>Lv. ${g.level} · ${g.member_count}/${g.max_members} Mitgl.</span>
|
||||
<span class="${g.open ? 'gh-guild-open' : 'gh-guild-closed'}">
|
||||
${g.open ? '✔ Offen' : '🔒 Anfrage'}
|
||||
</span>
|
||||
</div>
|
||||
<button class="gh-join-btn ${g.has_pending_request ? 'pending' : ''}"
|
||||
data-id="${g.id}" data-open="${g.open}"
|
||||
${gh_playerGuild || g.has_pending_request ? 'disabled' : ''}>
|
||||
${g.has_pending_request ? '⏳ Anfrage ausstehend'
|
||||
: gh_playerGuild === g.id ? '✔ Meine Gilde'
|
||||
: gh_playerGuild ? '(In Gilde)'
|
||||
: g.open ? '⚔ Beitreten' : '✉ Anfrage stellen'}
|
||||
</button>
|
||||
</div>`).join('');
|
||||
|
||||
grid.querySelectorAll('.gh-join-btn:not([disabled])').forEach(btn => {
|
||||
btn.addEventListener('click', () => ghJoinGuild(
|
||||
parseInt(btn.dataset.id), btn.dataset.open === '1', btn
|
||||
));
|
||||
});
|
||||
|
||||
ghRenderPagination(pagination, data.totalPages, data.total);
|
||||
} catch (err) {
|
||||
grid.innerHTML = '<div class="gh-empty">Fehler beim Laden.</div>';
|
||||
console.error('[GH]', err);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Gilde beitreten ────────────────────────────────────── */
|
||||
async function ghJoinGuild(guildId, isOpen, btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
|
||||
let message = null;
|
||||
if (!isOpen) {
|
||||
message = window.prompt('Nachricht an die Gilde (optional):') ?? '';
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/gildenhalle/join/${guildId}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) { alert(data.error || 'Fehler'); btn.disabled=false; btn.textContent=isOpen?'⚔ Beitreten':'✉ Anfrage stellen'; return; }
|
||||
|
||||
if (data.joined) {
|
||||
ghShowToast('✔ Erfolgreich der Gilde beigetreten!');
|
||||
gh_page = 1; ghLoadGuildList();
|
||||
} else {
|
||||
btn.classList.add('pending');
|
||||
btn.textContent = '⏳ Anfrage ausstehend';
|
||||
ghShowToast('✉ Beitrittsanfrage gesendet!');
|
||||
}
|
||||
} catch { btn.disabled=false; }
|
||||
}
|
||||
|
||||
/* ── Gilde gründen ──────────────────────────────────────── */
|
||||
async function ghCreateGuild() {
|
||||
const name = document.getElementById('gh-new-name').value.trim();
|
||||
const tag = document.getElementById('gh-new-tag').value.trim().toUpperCase();
|
||||
const desc = document.getElementById('gh-new-desc').value.trim();
|
||||
const open = document.getElementById('gh-new-open').value;
|
||||
const errEl = document.getElementById('gh-create-error');
|
||||
|
||||
errEl.textContent = '';
|
||||
if (!name || !tag) { errEl.textContent = 'Name und Tag sind Pflichtfelder.'; return; }
|
||||
|
||||
const btn = document.getElementById('gh-create-submit');
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/gildenhalle/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, tag, description: desc, open: open === '1' }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) { errEl.textContent = data.error || 'Fehler'; btn.disabled=false; btn.textContent='⚔ GILDE GRÜNDEN'; return; }
|
||||
|
||||
document.getElementById('gh-create-form-wrap').style.display = 'none';
|
||||
ghShowToast('🏰 Gilde erfolgreich gegründet!');
|
||||
gh_page = 1; ghLoadGuildList();
|
||||
|
||||
// Wechsel zu "Eigene Gilde"
|
||||
setTimeout(() => {
|
||||
document.querySelector('[data-tab="gh-panel-2"]')?.click();
|
||||
}, 500);
|
||||
} catch { btn.disabled=false; btn.textContent='⚔ GILDE GRÜNDEN'; }
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
PANEL 2: Eigene Gilde
|
||||
════════════════════════════════════════════ */
|
||||
async function ghLoadMyGuild() {
|
||||
const container = document.getElementById('gh-my-content');
|
||||
if (!container) return;
|
||||
container.innerHTML = '<div class="gh-loading">Lade…</div>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/gildenhalle/my');
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.guild) {
|
||||
container.innerHTML = `
|
||||
<div class="gh-no-guild">
|
||||
<div class="gh-no-guild-icon">🏰</div>
|
||||
<div class="gh-no-guild-text">Du bist noch in keiner Gilde.<br>
|
||||
Tritt einer Gilde bei oder gründe eine eigene.</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const { guild, members, ranks, requests } = data;
|
||||
const isLeader = (guild.leader_id === /* wird per Session gesetzt */ null);
|
||||
|
||||
// Mitglieder-Tabelle
|
||||
const membersHtml = members.map(m => `
|
||||
<tr>
|
||||
<td>${m.name}</td>
|
||||
<td>Lv. ${m.level}</td>
|
||||
<td>
|
||||
${guild.can_manage_ranks && m.id !== guild.leader_id
|
||||
? `<select class="gh-rank-select" data-uid="${m.id}" onchange="ghChangeRank(${m.id},this.value)">
|
||||
${ranks.map(r =>
|
||||
`<option value="${r.id}" ${r.id === m.rank_id ? 'selected' : ''}>${r.name}</option>`
|
||||
).join('')}
|
||||
</select>`
|
||||
: `<span class="gh-rank-badge ${m.id === guild.leader_id ? 'leader' : ''}">${m.rank_name || '–'}</span>`
|
||||
}
|
||||
</td>
|
||||
<td style="font-size:10px;color:var(--guild-muted)">
|
||||
${new Date(m.joined_at).toLocaleDateString('de-DE')}
|
||||
</td>
|
||||
<td>
|
||||
${guild.can_kick && m.id !== guild.leader_id
|
||||
? `<button class="gh-kick-btn" onclick="ghKickMember(${m.id},'${m.name}')">Kick</button>`
|
||||
: ''}
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
|
||||
// Anfragen
|
||||
const requestsHtml = requests.length > 0
|
||||
? `<div class="gh-section-title">Beitrittsanfragen (${requests.length})</div>
|
||||
<div class="gh-requests-list">
|
||||
${requests.map(r => `
|
||||
<div class="gh-request-row">
|
||||
<span class="gh-request-name">${r.name}</span>
|
||||
<span class="gh-request-msg">${r.message || '–'}</span>
|
||||
${guild.can_invite
|
||||
? `<button class="gh-req-accept" onclick="ghHandleRequest(${r.id},'accept')">✔</button>
|
||||
<button class="gh-req-reject" onclick="ghHandleRequest(${r.id},'reject')">✕</button>`
|
||||
: ''}
|
||||
</div>`).join('')}
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="gh-my-info">
|
||||
<span class="gh-my-tag">[${guild.tag}]</span>
|
||||
<div>
|
||||
<div class="gh-my-name">${guild.name}</div>
|
||||
<div class="gh-my-sub">Lv. ${guild.level} · ${members.length}/${guild.max_members} Mitglieder</div>
|
||||
</div>
|
||||
<button class="gh-leave-btn" onclick="ghLeaveGuild()">Austreten</button>
|
||||
</div>
|
||||
${requestsHtml}
|
||||
<div class="gh-section-title">Mitglieder</div>
|
||||
<div style="padding:0 4px;overflow-x:auto;">
|
||||
<table class="gh-members-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th><th>Level</th><th>Rang</th><th>Beigetreten</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${membersHtml}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
} catch (err) {
|
||||
container.innerHTML = '<div class="gh-empty">Fehler beim Laden.</div>';
|
||||
console.error('[GH/my]', err);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Rang ändern ────────────────────────────────────────── */
|
||||
window.ghChangeRank = async function(userId, rankId) {
|
||||
try {
|
||||
const res = await fetch(`/api/gildenhalle/member/${userId}/rank`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ rank_id: rankId }),
|
||||
});
|
||||
if (!res.ok) { const d=await res.json(); alert(d.error||'Fehler'); ghLoadMyGuild(); }
|
||||
else ghShowToast('✔ Rang geändert');
|
||||
} catch {}
|
||||
};
|
||||
|
||||
/* ── Mitglied kicken ────────────────────────────────────── */
|
||||
window.ghKickMember = async function(userId, name) {
|
||||
if (!confirm(`${name} aus der Gilde entfernen?`)) return;
|
||||
try {
|
||||
const res = await fetch(`/api/gildenhalle/member/${userId}/kick`, { method: 'POST' });
|
||||
const d = await res.json();
|
||||
if (!res.ok) { alert(d.error||'Fehler'); return; }
|
||||
ghShowToast(`✔ ${name} wurde entfernt`);
|
||||
ghLoadMyGuild();
|
||||
} catch {}
|
||||
};
|
||||
|
||||
/* ── Anfrage annehmen/ablehnen ──────────────────────────── */
|
||||
window.ghHandleRequest = async function(requestId, action) {
|
||||
try {
|
||||
const res = await fetch(`/api/gildenhalle/requests/${requestId}/${action}`, { method: 'POST' });
|
||||
const d = await res.json();
|
||||
if (!res.ok) { alert(d.error||'Fehler'); return; }
|
||||
ghShowToast(action === 'accept' ? '✔ Anfrage angenommen' : '✕ Anfrage abgelehnt');
|
||||
ghLoadMyGuild();
|
||||
} catch {}
|
||||
};
|
||||
|
||||
/* ── Gilde verlassen ────────────────────────────────────── */
|
||||
window.ghLeaveGuild = async function() {
|
||||
if (!confirm('Wirklich die Gilde verlassen?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/gildenhalle/leave', { method: 'POST' });
|
||||
const d = await res.json();
|
||||
if (!res.ok) { alert(d.error||'Fehler'); return; }
|
||||
ghShowToast('Gilde verlassen');
|
||||
ghLoadMyGuild();
|
||||
gh_page = 1; ghLoadGuildList();
|
||||
} catch {}
|
||||
};
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
PANEL 3: Aufgaben
|
||||
════════════════════════════════════════════ */
|
||||
async function ghLoadTasks() {
|
||||
const container = document.getElementById('gh-tasks-content');
|
||||
if (!container) return;
|
||||
container.innerHTML = '<div class="gh-loading">Lade Aufgaben…</div>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/gildenhalle/tasks');
|
||||
if (res.status === 400) {
|
||||
container.innerHTML = `
|
||||
<div class="gh-no-guild">
|
||||
<div class="gh-no-guild-icon">📋</div>
|
||||
<div class="gh-no-guild-text">Tritt einer Gilde bei um gemeinsame Aufgaben zu sehen.</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.tasks?.length) {
|
||||
container.innerHTML = '<div class="gh-empty">Keine Aufgaben für heute.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const rewardIcons = { gold: '🪙', gems: '💎', wood: '🪵', iron: '⚙️' };
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="padding:10px 16px;font-family:'Cinzel',serif;font-size:11px;
|
||||
color:var(--guild-muted);border-bottom:1px solid rgba(139,106,42,.25);">
|
||||
Gemeinsame Aufgaben der Gilde <strong style="color:var(--guild-gold);">${data.guildName}</strong>
|
||||
</div>
|
||||
<div class="gh-tasks-list">
|
||||
${data.tasks.map(t => {
|
||||
const pct = Math.min(100, Math.round((t.current_amount / t.target_amount) * 100));
|
||||
const done = t.completed || t.current_amount >= t.target_amount;
|
||||
const icon = rewardIcons[t.reward_type] || '🪙';
|
||||
return `
|
||||
<div class="gh-task-card ${done ? 'completed' : ''}">
|
||||
<div class="gh-task-header">
|
||||
<span class="gh-task-label">${t.label}</span>
|
||||
${done
|
||||
? `<span class="gh-task-done-badge">✔ Erledigt</span>`
|
||||
: `<span class="gh-task-reward">${icon} ${t.reward_amount} ${t.reward_type}</span>`}
|
||||
</div>
|
||||
<div class="gh-task-progress-wrap">
|
||||
<div class="gh-task-bar-track">
|
||||
<div class="gh-task-bar-fill ${done?'completed':''}"
|
||||
style="width:${pct}%"></div>
|
||||
</div>
|
||||
<div class="gh-task-progress-text">
|
||||
<span>${t.current_amount} / ${t.target_amount}</span>
|
||||
<span>${pct}% · Mein Beitrag: ${t.my_contribution || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>`;
|
||||
} catch (err) {
|
||||
container.innerHTML = '<div class="gh-empty">Fehler beim Laden.</div>';
|
||||
console.error('[GH/tasks]', err);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Pagination ─────────────────────────────────────────── */
|
||||
function ghRenderPagination(el, totalPages, total) {
|
||||
if (!el || !totalPages || totalPages <= 1) return;
|
||||
el.innerHTML = `
|
||||
<button class="gh-page-btn" id="gh-prev" ${gh_page===1?'disabled':''}>◀</button>
|
||||
${Array.from({length:totalPages},(_,i)=>i+1).map(p =>
|
||||
`<button class="gh-page-btn ${p===gh_page?'active':''}" data-page="${p}">${p}</button>`
|
||||
).join('')}
|
||||
<button class="gh-page-btn" id="gh-next" ${gh_page===totalPages?'disabled':''}>▶</button>`;
|
||||
el.querySelector('#gh-prev')?.addEventListener('click', () => { if(gh_page>1){gh_page--;ghLoadGuildList();} });
|
||||
el.querySelector('#gh-next')?.addEventListener('click', () => { if(gh_page<totalPages){gh_page++;ghLoadGuildList();} });
|
||||
el.querySelectorAll('[data-page]').forEach(btn =>
|
||||
btn.addEventListener('click', () => { gh_page=parseInt(btn.dataset.page); ghLoadGuildList(); })
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Toast ──────────────────────────────────────────────── */
|
||||
function ghShowToast(msg) {
|
||||
const t = document.createElement('div');
|
||||
t.className = 'baz-toast'; t.textContent = msg;
|
||||
document.body.appendChild(t);
|
||||
setTimeout(() => t.remove(), 2800);
|
||||
}
|
||||
|
||||
/* ── Schließen ──────────────────────────────────────────── */
|
||||
function ghClose() {
|
||||
document.getElementById('gildenhalle-popup')?.classList.remove('active');
|
||||
document.getElementById('qm-overlay')?.classList.remove('active');
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
EXPORT
|
||||
════════════════════════════════════════════ */
|
||||
export function loadGildenhalle() {
|
||||
ghLoadCSS();
|
||||
ghEnsurePopup();
|
||||
|
||||
const popup = document.getElementById('gildenhalle-popup');
|
||||
const overlay = document.getElementById('qm-overlay');
|
||||
popup.style.left = '50%'; popup.style.top = '50%';
|
||||
popup.style.transform = 'translate(-50%,-50%) scale(1)';
|
||||
popup.classList.add('active');
|
||||
overlay?.classList.add('active');
|
||||
|
||||
if (!gh_initialized) {
|
||||
gh_initialized = true;
|
||||
ghLoadGuildList();
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,8 @@ import { loadBlackmarket } from "./buildings/blackmarket.js?v=2";
|
||||
import { loadMine } from "./buildings/mine.js?v=2";
|
||||
import { loadGlucksspiel } from "./gaststaette/glucksspiel.js";
|
||||
import { loadBazaar } from "./buildings/bazaar.js";
|
||||
import { loadGildenhalle } from "./buildings/gildenhalle.js";
|
||||
|
||||
const popup = document.getElementById("building-popup");
|
||||
const title = document.getElementById("popup-title");
|
||||
const tooltip = document.getElementById("map-tooltip");
|
||||
@ -16,12 +18,16 @@ const tooltipCache = {};
|
||||
================================ */
|
||||
|
||||
const gaststaettePopup = document.getElementById("gaststaette-popup");
|
||||
const qmOverlay = document.getElementById("qm-overlay");
|
||||
const qmOverlay = document.getElementById("qm-overlay");
|
||||
|
||||
document.querySelectorAll(".gaststaette-tab").forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
document.querySelectorAll(".gaststaette-tab").forEach((t) => t.classList.remove("active"));
|
||||
document.querySelectorAll(".gaststaette-tab-content").forEach((c) => c.classList.remove("active"));
|
||||
document
|
||||
.querySelectorAll(".gaststaette-tab")
|
||||
.forEach((t) => t.classList.remove("active"));
|
||||
document
|
||||
.querySelectorAll(".gaststaette-tab-content")
|
||||
.forEach((c) => c.classList.remove("active"));
|
||||
tab.classList.add("active");
|
||||
document.getElementById(tab.dataset.tab).classList.add("active");
|
||||
if (tab.dataset.tab === "gs-tab1") loadGlucksspiel();
|
||||
@ -49,7 +55,7 @@ document.getElementById("qm-overlay")?.addEventListener("mouseenter", () => {
|
||||
});
|
||||
|
||||
const buildingModules = {
|
||||
1: loadArena, // Arena – eigenes UI, Tabs ausblenden
|
||||
1: loadArena, // Arena – eigenes UI, Tabs ausblenden
|
||||
3: loadHimmelstor, // Himmelstor – eigenes UI, Tabs ausblenden
|
||||
10: loadMine, // Mine – Tabs bleiben sichtbar
|
||||
11: loadCharacterHouse, // Charakterhaus – eigenes UI, Tabs ausblenden
|
||||
@ -182,7 +188,7 @@ document.querySelectorAll(".building").forEach((building) => {
|
||||
// Gebäude 6 → Gaststätte eigenes Popup
|
||||
if (url === "/building/6") {
|
||||
gaststaettePopup.style.left = "50%";
|
||||
gaststaettePopup.style.top = "50%";
|
||||
gaststaettePopup.style.top = "50%";
|
||||
gaststaettePopup.style.transform = "translate(-50%, -50%) scale(1)";
|
||||
gaststaettePopup.classList.add("active");
|
||||
qmOverlay.classList.add("active");
|
||||
@ -196,6 +202,12 @@ document.querySelectorAll(".building").forEach((building) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Gebäude 9 → Gildenhalle eigenes Popup
|
||||
if (url === "/building/9") {
|
||||
loadGildenhalle();
|
||||
return;
|
||||
}
|
||||
|
||||
await openBuildingPopup(url);
|
||||
});
|
||||
});
|
||||
@ -287,7 +299,8 @@ document.querySelectorAll(".building").forEach((building) => {
|
||||
building.addEventListener("mouseenter", async (e) => {
|
||||
try {
|
||||
// data-id bevorzugen, sonst ID aus href extrahieren (z.B. "/building/5" → "5")
|
||||
const id = building.dataset.id || building.getAttribute("href")?.split("/").pop();
|
||||
const id =
|
||||
building.dataset.id || building.getAttribute("href")?.split("/").pop();
|
||||
if (!id) return;
|
||||
|
||||
if (!tooltipCache[id]) {
|
||||
|
||||
414
routes/gildenhalle.route.js
Normal file
414
routes/gildenhalle.route.js
Normal file
@ -0,0 +1,414 @@
|
||||
/* ============================================================
|
||||
routes/gildenhalle.route.js
|
||||
============================================================ */
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../database/database');
|
||||
|
||||
function requireLogin(req, res, next) {
|
||||
if (!req.session?.user) return res.status(401).json({ error: 'Nicht eingeloggt' });
|
||||
next();
|
||||
}
|
||||
|
||||
/* ── Hilfsfunktion: Spieler-Gilde laden ────────────────── */
|
||||
async function getPlayerGuild(userId) {
|
||||
const [[row]] = await db.query(
|
||||
`SELECT g.*, gm.rank_id, gr.name AS rank_name, gr.sort_order,
|
||||
gr.can_invite, gr.can_kick, gr.can_manage_ranks, gr.can_manage_tasks
|
||||
FROM guild_members gm
|
||||
JOIN guilds g ON g.id = gm.guild_id
|
||||
LEFT JOIN guild_ranks gr ON gr.id = gm.rank_id
|
||||
WHERE gm.user_id = ?`,
|
||||
[userId]
|
||||
);
|
||||
return row || null;
|
||||
}
|
||||
|
||||
/* ── Standard-Ränge für neue Gilde anlegen ─────────────── */
|
||||
async function createDefaultRanks(guildId) {
|
||||
const ranks = [
|
||||
{ name: 'Gildenmeister', sort_order: 1, can_invite: 1, can_kick: 1, can_manage_ranks: 1, can_manage_tasks: 1 },
|
||||
{ name: 'Offizier', sort_order: 2, can_invite: 1, can_kick: 1, can_manage_ranks: 0, can_manage_tasks: 1 },
|
||||
{ name: 'Veteran', sort_order: 3, can_invite: 1, can_kick: 0, can_manage_ranks: 0, can_manage_tasks: 0 },
|
||||
{ name: 'Mitglied', sort_order: 99, can_invite: 0, can_kick: 0, can_manage_ranks: 0, can_manage_tasks: 0 },
|
||||
];
|
||||
const ids = [];
|
||||
for (const r of ranks) {
|
||||
const [res] = await db.query(
|
||||
`INSERT INTO guild_ranks (guild_id, name, sort_order, can_invite, can_kick, can_manage_ranks, can_manage_tasks)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[guildId, r.name, r.sort_order, r.can_invite, r.can_kick, r.can_manage_ranks, r.can_manage_tasks]
|
||||
);
|
||||
ids.push({ name: r.name, id: res.insertId });
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/* ── Tagesaufgaben für Gilde generieren ────────────────── */
|
||||
async function ensureDailyTasks(guildId) {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const [existing] = await db.query(
|
||||
'SELECT id FROM guild_tasks WHERE guild_id = ? AND expires_at = ?',
|
||||
[guildId, today]
|
||||
);
|
||||
if (existing.length > 0) return;
|
||||
|
||||
const [templates] = await db.query(
|
||||
'SELECT * FROM guild_task_templates WHERE active = 1'
|
||||
);
|
||||
for (const t of templates) {
|
||||
await db.query(
|
||||
`INSERT IGNORE INTO guild_tasks
|
||||
(guild_id, task_key, label, target_amount, reward_type, reward_amount, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[guildId, t.task_key, t.label, t.target_amount, t.reward_type, t.reward_amount, today]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
GET /api/gildenhalle/list?search=&page=1
|
||||
Alle Gilden auflisten
|
||||
════════════════════════════════════════════ */
|
||||
router.get('/gildenhalle/list', requireLogin, async (req, res) => {
|
||||
const search = req.query.search?.trim() || '';
|
||||
const page = Math.max(1, parseInt(req.query.page) || 1);
|
||||
const limit = 12;
|
||||
const offset = (page - 1) * limit;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
try {
|
||||
const searchParam = search ? `%${search}%` : '%';
|
||||
const [guilds] = await db.query(
|
||||
`SELECT g.id, g.name, g.tag, g.description, g.level, g.open, g.max_members,
|
||||
COUNT(gm.id) AS member_count,
|
||||
a.ingame_name AS leader_name
|
||||
FROM guilds g
|
||||
LEFT JOIN guild_members gm ON gm.guild_id = g.id
|
||||
LEFT JOIN accounts a ON a.id = g.leader_id
|
||||
WHERE g.name LIKE ? OR g.tag LIKE ?
|
||||
GROUP BY g.id
|
||||
ORDER BY g.level DESC, member_count DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
[searchParam, searchParam, limit, offset]
|
||||
);
|
||||
|
||||
const [[{ total }]] = await db.query(
|
||||
'SELECT COUNT(*) AS total FROM guilds WHERE name LIKE ? OR tag LIKE ?',
|
||||
[searchParam, searchParam]
|
||||
);
|
||||
|
||||
// Offene Anfrage des Spielers prüfen
|
||||
const [myRequests] = await db.query(
|
||||
"SELECT guild_id FROM guild_join_requests WHERE user_id = ? AND status = 'pending'",
|
||||
[userId]
|
||||
);
|
||||
const pendingIds = new Set(myRequests.map(r => r.guild_id));
|
||||
|
||||
const playerGuild = await getPlayerGuild(userId);
|
||||
|
||||
res.json({
|
||||
guilds: guilds.map(g => ({
|
||||
...g,
|
||||
has_pending_request: pendingIds.has(g.id),
|
||||
})),
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
page,
|
||||
playerGuildId: playerGuild?.id || null,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[gildenhalle/list]', err);
|
||||
res.status(500).json({ error: 'DB Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
POST /api/gildenhalle/create
|
||||
Neue Gilde gründen
|
||||
════════════════════════════════════════════ */
|
||||
router.post('/gildenhalle/create', requireLogin, async (req, res) => {
|
||||
const userId = req.session.user.id;
|
||||
const { name, tag, description, open } = req.body;
|
||||
|
||||
if (!name?.trim() || !tag?.trim())
|
||||
return res.status(400).json({ error: 'Name und Tag sind Pflichtfelder.' });
|
||||
if (tag.length < 2 || tag.length > 6)
|
||||
return res.status(400).json({ error: 'Tag muss 2–6 Zeichen lang sein.' });
|
||||
|
||||
try {
|
||||
const existing = await getPlayerGuild(userId);
|
||||
if (existing) return res.status(400).json({ error: 'Du bist bereits in einer Gilde.' });
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO guilds (name, tag, description, leader_id, open)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[name.trim(), tag.trim().toUpperCase(), description?.trim() || null, userId, open ? 1 : 0]
|
||||
);
|
||||
const guildId = result.insertId;
|
||||
|
||||
// Standard-Ränge anlegen
|
||||
const rankIds = await createDefaultRanks(guildId);
|
||||
const leaderRankId = rankIds.find(r => r.name === 'Gildenmeister')?.id;
|
||||
|
||||
// Gründer als Mitglied mit höchstem Rang eintragen
|
||||
await db.query(
|
||||
'INSERT INTO guild_members (guild_id, user_id, rank_id) VALUES (?, ?, ?)',
|
||||
[guildId, userId, leaderRankId]
|
||||
);
|
||||
|
||||
// Erste Tagesaufgaben generieren
|
||||
await ensureDailyTasks(guildId);
|
||||
|
||||
res.json({ success: true, guildId });
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY')
|
||||
return res.status(400).json({ error: 'Gildenname oder Tag bereits vergeben.' });
|
||||
console.error('[gildenhalle/create]', err);
|
||||
res.status(500).json({ error: 'DB Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
POST /api/gildenhalle/join/:id
|
||||
Gilde beitreten oder Anfrage stellen
|
||||
════════════════════════════════════════════ */
|
||||
router.post('/gildenhalle/join/:id', requireLogin, async (req, res) => {
|
||||
const userId = req.session.user.id;
|
||||
const guildId = parseInt(req.params.id);
|
||||
|
||||
try {
|
||||
const existing = await getPlayerGuild(userId);
|
||||
if (existing) return res.status(400).json({ error: 'Du bist bereits in einer Gilde.' });
|
||||
|
||||
const [[guild]] = await db.query('SELECT * FROM guilds WHERE id = ?', [guildId]);
|
||||
if (!guild) return res.status(404).json({ error: 'Gilde nicht gefunden.' });
|
||||
|
||||
const [[memberCount]] = await db.query(
|
||||
'SELECT COUNT(*) AS c FROM guild_members WHERE guild_id = ?', [guildId]
|
||||
);
|
||||
if (memberCount.c >= guild.max_members)
|
||||
return res.status(400).json({ error: 'Gilde ist voll.' });
|
||||
|
||||
if (guild.open) {
|
||||
// Standard-Rang (Mitglied = höchste sort_order)
|
||||
const [[defaultRank]] = await db.query(
|
||||
'SELECT id FROM guild_ranks WHERE guild_id = ? ORDER BY sort_order DESC LIMIT 1',
|
||||
[guildId]
|
||||
);
|
||||
await db.query(
|
||||
'INSERT INTO guild_members (guild_id, user_id, rank_id) VALUES (?, ?, ?)',
|
||||
[guildId, userId, defaultRank?.id || null]
|
||||
);
|
||||
res.json({ success: true, joined: true });
|
||||
} else {
|
||||
// Anfrage stellen
|
||||
await db.query(
|
||||
`INSERT IGNORE INTO guild_join_requests (guild_id, user_id, message)
|
||||
VALUES (?, ?, ?)`,
|
||||
[guildId, userId, req.body.message?.trim() || null]
|
||||
);
|
||||
res.json({ success: true, joined: false, requested: true });
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY')
|
||||
return res.status(400).json({ error: 'Anfrage bereits gestellt.' });
|
||||
console.error('[gildenhalle/join]', err);
|
||||
res.status(500).json({ error: 'DB Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
POST /api/gildenhalle/leave
|
||||
Gilde verlassen
|
||||
════════════════════════════════════════════ */
|
||||
router.post('/gildenhalle/leave', requireLogin, async (req, res) => {
|
||||
const userId = req.session.user.id;
|
||||
try {
|
||||
const guild = await getPlayerGuild(userId);
|
||||
if (!guild) return res.status(400).json({ error: 'Du bist in keiner Gilde.' });
|
||||
if (guild.leader_id === userId)
|
||||
return res.status(400).json({ error: 'Als Gildenleiterin kannst du nicht austreten. Übertrage zuerst die Leitung.' });
|
||||
|
||||
await db.query('DELETE FROM guild_members WHERE user_id = ? AND guild_id = ?', [userId, guild.id]);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[gildenhalle/leave]', err);
|
||||
res.status(500).json({ error: 'DB Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
GET /api/gildenhalle/my
|
||||
Eigene Gilde + Mitglieder laden
|
||||
════════════════════════════════════════════ */
|
||||
router.get('/gildenhalle/my', requireLogin, async (req, res) => {
|
||||
const userId = req.session.user.id;
|
||||
try {
|
||||
const guild = await getPlayerGuild(userId);
|
||||
if (!guild) return res.json({ guild: null });
|
||||
|
||||
await ensureDailyTasks(guild.id);
|
||||
|
||||
const [members] = await db.query(
|
||||
`SELECT a.id, a.ingame_name AS name, a.level,
|
||||
gr.id AS rank_id, gr.name AS rank_name, gr.sort_order,
|
||||
gm.joined_at
|
||||
FROM guild_members gm
|
||||
JOIN accounts a ON a.id = gm.user_id
|
||||
LEFT JOIN guild_ranks gr ON gr.id = gm.rank_id
|
||||
WHERE gm.guild_id = ?
|
||||
ORDER BY gr.sort_order ASC, gm.joined_at ASC`,
|
||||
[guild.id]
|
||||
);
|
||||
|
||||
const [ranks] = await db.query(
|
||||
'SELECT * FROM guild_ranks WHERE guild_id = ? ORDER BY sort_order ASC',
|
||||
[guild.id]
|
||||
);
|
||||
|
||||
const [requests] = await db.query(
|
||||
`SELECT gjr.id, gjr.user_id, a.ingame_name AS name, gjr.message, gjr.created_at
|
||||
FROM guild_join_requests gjr
|
||||
JOIN accounts a ON a.id = gjr.user_id
|
||||
WHERE gjr.guild_id = ? AND gjr.status = 'pending'`,
|
||||
[guild.id]
|
||||
);
|
||||
|
||||
res.json({ guild, members, ranks, requests });
|
||||
} catch (err) {
|
||||
console.error('[gildenhalle/my]', err);
|
||||
res.status(500).json({ error: 'DB Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
GET /api/gildenhalle/tasks
|
||||
Gilden-Tagesaufgaben laden
|
||||
════════════════════════════════════════════ */
|
||||
router.get('/gildenhalle/tasks', requireLogin, async (req, res) => {
|
||||
const userId = req.session.user.id;
|
||||
try {
|
||||
const guild = await getPlayerGuild(userId);
|
||||
if (!guild) return res.status(400).json({ error: 'Du bist in keiner Gilde.' });
|
||||
|
||||
await ensureDailyTasks(guild.id);
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
const [tasks] = await db.query(
|
||||
`SELECT t.*,
|
||||
COALESCE(SUM(c.amount), 0) AS contributed_total,
|
||||
(SELECT COALESCE(SUM(c2.amount),0) FROM guild_task_contributions c2
|
||||
WHERE c2.task_id = t.id AND c2.user_id = ?) AS my_contribution
|
||||
FROM guild_tasks t
|
||||
LEFT JOIN guild_task_contributions c ON c.task_id = t.id
|
||||
WHERE t.guild_id = ? AND t.expires_at = ?
|
||||
GROUP BY t.id`,
|
||||
[userId, guild.id, today]
|
||||
);
|
||||
|
||||
res.json({ tasks, guildName: guild.name });
|
||||
} catch (err) {
|
||||
console.error('[gildenhalle/tasks]', err);
|
||||
res.status(500).json({ error: 'DB Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
POST /api/gildenhalle/requests/:id/accept
|
||||
POST /api/gildenhalle/requests/:id/reject
|
||||
Beitrittsanfragen verwalten
|
||||
════════════════════════════════════════════ */
|
||||
router.post('/gildenhalle/requests/:id/:action', requireLogin, async (req, res) => {
|
||||
const userId = req.session.user.id;
|
||||
const requestId = parseInt(req.params.id);
|
||||
const action = req.params.action; // accept | reject
|
||||
|
||||
if (!['accept', 'reject'].includes(action))
|
||||
return res.status(400).json({ error: 'Ungültige Aktion.' });
|
||||
|
||||
try {
|
||||
const guild = await getPlayerGuild(userId);
|
||||
if (!guild?.can_invite) return res.status(403).json({ error: 'Keine Berechtigung.' });
|
||||
|
||||
const [[request]] = await db.query(
|
||||
"SELECT * FROM guild_join_requests WHERE id = ? AND guild_id = ? AND status = 'pending'",
|
||||
[requestId, guild.id]
|
||||
);
|
||||
if (!request) return res.status(404).json({ error: 'Anfrage nicht gefunden.' });
|
||||
|
||||
await db.query(
|
||||
'UPDATE guild_join_requests SET status = ? WHERE id = ?',
|
||||
[action === 'accept' ? 'accepted' : 'rejected', requestId]
|
||||
);
|
||||
|
||||
if (action === 'accept') {
|
||||
const [[defaultRank]] = await db.query(
|
||||
'SELECT id FROM guild_ranks WHERE guild_id = ? ORDER BY sort_order DESC LIMIT 1',
|
||||
[guild.id]
|
||||
);
|
||||
await db.query(
|
||||
'INSERT IGNORE INTO guild_members (guild_id, user_id, rank_id) VALUES (?, ?, ?)',
|
||||
[guild.id, request.user_id, defaultRank?.id || null]
|
||||
);
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[gildenhalle/requests]', err);
|
||||
res.status(500).json({ error: 'DB Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
POST /api/gildenhalle/member/:userId/rank
|
||||
Rang eines Mitglieds ändern
|
||||
════════════════════════════════════════════ */
|
||||
router.post('/gildenhalle/member/:targetId/rank', requireLogin, async (req, res) => {
|
||||
const userId = req.session.user.id;
|
||||
const targetId = parseInt(req.params.targetId);
|
||||
const { rank_id } = req.body;
|
||||
|
||||
try {
|
||||
const guild = await getPlayerGuild(userId);
|
||||
if (!guild?.can_manage_ranks) return res.status(403).json({ error: 'Keine Berechtigung.' });
|
||||
if (targetId === guild.leader_id) return res.status(403).json({ error: 'Rang des Gildenmeisters kann nicht geändert werden.' });
|
||||
|
||||
await db.query(
|
||||
'UPDATE guild_members SET rank_id = ? WHERE user_id = ? AND guild_id = ?',
|
||||
[rank_id, targetId, guild.id]
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[gildenhalle/member/rank]', err);
|
||||
res.status(500).json({ error: 'DB Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
POST /api/gildenhalle/member/:userId/kick
|
||||
Mitglied entfernen
|
||||
════════════════════════════════════════════ */
|
||||
router.post('/gildenhalle/member/:targetId/kick', requireLogin, async (req, res) => {
|
||||
const userId = req.session.user.id;
|
||||
const targetId = parseInt(req.params.targetId);
|
||||
|
||||
try {
|
||||
const guild = await getPlayerGuild(userId);
|
||||
if (!guild?.can_kick) return res.status(403).json({ error: 'Keine Berechtigung.' });
|
||||
if (targetId === guild.leader_id) return res.status(403).json({ error: 'Gildenmeister kann nicht gekickt werden.' });
|
||||
|
||||
await db.query(
|
||||
'DELETE FROM guild_members WHERE user_id = ? AND guild_id = ?',
|
||||
[targetId, guild.id]
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[gildenhalle/member/kick]', err);
|
||||
res.status(500).json({ error: 'DB Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -97,6 +97,7 @@
|
||||
<link rel="stylesheet" href="/css/daily.css" />
|
||||
<link rel="stylesheet" href="/css/events.css" />
|
||||
<link rel="stylesheet" href="/css/hud.css" />
|
||||
<link rel="stylesheet" href="/css/gildenhalle.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user