Add reputation actions feature to Falukant module

- Introduced new endpoints for retrieving and executing reputation actions in FalukantController and falukantRouter.
- Implemented service methods in FalukantService to handle reputation actions, including daily limits and action execution logic.
- Updated the frontend ReputationView component to display available actions and their details, including cost and potential reputation gain.
- Added translations for reputation actions in both German and English locales.
- Enhanced initialization logic to set up reputation action types in the database.
This commit is contained in:
Torsten Schulz (local)
2025-12-21 21:09:31 +01:00
parent 38f23cc6ae
commit 38dd51f757
13 changed files with 594 additions and 2 deletions

View File

@@ -782,6 +782,33 @@
"type": "Festart",
"cost": "Kosten",
"date": "Datum"
},
"actions": {
"title": "Aktionen",
"description": "Mit diesen Aktionen kannst du Reputation gewinnen. Je öfter du dieselbe Aktion ausführst, desto weniger Reputation bringt sie (unabhängig von den Kosten).",
"action": "Aktion",
"cost": "Kosten",
"gain": "Reputation",
"timesUsed": "Bereits genutzt",
"execute": "Ausführen",
"running": "Läuft...",
"none": "Keine Aktionen verfügbar.",
"dailyLimit": "Heute noch verfügbar: {remaining} / {cap} Reputation (durch Aktionen).",
"success": "Aktion erfolgreich! Reputation +{gain}, Kosten {cost}.",
"successSimple": "Aktion erfolgreich!",
"type": {
"library_donation": "Spende für eine Bibliothek",
"orphanage_build": "Waisenhaus aufbauen",
"statue_build": "Statue errichten",
"hospital_donation": "Krankenhaus/Heilhaus stiften",
"school_funding": "Schule/Lehrstuhl finanzieren",
"well_build": "Brunnen/Wasserwerk bauen",
"bridge_build": "Straßen-/Brückenbau finanzieren",
"soup_kitchen": "Armenspeisung organisieren",
"patronage": "Kunst & Mäzenatentum",
"church_hospice": "Hospiz-/Kirchenspende",
"scholarships": "Stipendienfonds finanzieren"
}
}
},
"party": {

View File

@@ -206,6 +206,33 @@
},
"party": {
"title": "Parties"
},
"actions": {
"title": "Actions",
"description": "These actions let you gain reputation. The more often you repeat the same action, the less reputation it yields (independent of cost).",
"action": "Action",
"cost": "Cost",
"gain": "Reputation",
"timesUsed": "Times used",
"execute": "Execute",
"running": "Running...",
"none": "No actions available.",
"dailyLimit": "Available today: {remaining} / {cap} reputation (from actions).",
"success": "Action successful! Reputation +{gain}, cost {cost}.",
"successSimple": "Action successful!",
"type": {
"library_donation": "Donate to a library",
"orphanage_build": "Build an orphanage",
"statue_build": "Erect a statue",
"hospital_donation": "Found a hospital/infirmary",
"school_funding": "Fund a school/chair",
"well_build": "Build a well/waterworks",
"bridge_build": "Fund roads/bridges",
"soup_kitchen": "Organize a soup kitchen",
"patronage": "Arts & patronage",
"church_hospice": "Hospice/church donation",
"scholarships": "Fund scholarships"
}
}
},
"branchProduction": {

View File

@@ -142,6 +142,44 @@
</table>
</div>
</div>
<div v-else-if="activeTab === 'actions'">
<p>
{{ $t('falukant.reputation.actions.description') }}
</p>
<p v-if="reputationActionsDailyCap != null" class="reputation-actions-daily">
{{ $t('falukant.reputation.actions.dailyLimit', { remaining: reputationActionsDailyRemaining, cap: reputationActionsDailyCap }) }}
</p>
<table v-if="reputationActions.length">
<thead>
<tr>
<th>{{ $t('falukant.reputation.actions.action') }}</th>
<th>{{ $t('falukant.reputation.actions.cost') }}</th>
<th>{{ $t('falukant.reputation.actions.gain') }}</th>
<th>{{ $t('falukant.reputation.actions.timesUsed') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="a in reputationActions" :key="a.id">
<td>{{ $t('falukant.reputation.actions.type.' + a.tr) }}</td>
<td>{{ Number(a.cost || 0).toLocaleString($i18n.locale) }}</td>
<td>+{{ Number(a.currentGain || 0) }}</td>
<td>{{ Number(a.timesUsed || 0) }}</td>
<td>
<button type="button" :disabled="runningActionId === a.id"
@click.prevent="executeReputationAction(a)">
{{ runningActionId === a.id ? $t('falukant.reputation.actions.running') : $t('falukant.reputation.actions.execute') }}
</button>
</td>
</tr>
</tbody>
</table>
<p v-else>
{{ $t('falukant.reputation.actions.none') }}
</p>
</div>
</div>
</div>
</template>
@@ -159,7 +197,8 @@ export default {
activeTab: 'overview',
tabs: [
{ value: 'overview', label: 'falukant.reputation.overview.title' },
{ value: 'party', label: 'falukant.reputation.party.title' }
{ value: 'party', label: 'falukant.reputation.party.title' },
{ value: 'actions', label: 'falukant.reputation.actions.title' }
],
newPartyView: false,
newPartyTypeId: null,
@@ -174,6 +213,11 @@ export default {
inProgressParties: [],
completedParties: [],
reputation: null,
reputationActions: [],
reputationActionsDailyCap: null,
reputationActionsDailyUsed: null,
reputationActionsDailyRemaining: null,
runningActionId: null,
}
},
methods: {
@@ -211,6 +255,41 @@ export default {
this.reputation = null;
}
},
async loadReputationActions() {
try {
const { data } = await apiClient.get('/api/falukant/reputation/actions');
this.reputationActionsDailyCap = data?.dailyCap ?? null;
this.reputationActionsDailyUsed = data?.dailyUsed ?? null;
this.reputationActionsDailyRemaining = data?.dailyRemaining ?? null;
this.reputationActions = Array.isArray(data?.actions) ? data.actions : [];
} catch (e) {
console.error('Failed to load reputation actions', e);
this.reputationActions = [];
this.reputationActionsDailyCap = null;
this.reputationActionsDailyUsed = null;
this.reputationActionsDailyRemaining = null;
}
},
async executeReputationAction(action) {
if (!action?.id) return;
if (this.runningActionId) return;
this.runningActionId = action.id;
try {
const { data } = await apiClient.post('/api/falukant/reputation/actions', { actionTypeId: action.id });
const gain = data?.gain ?? null;
const cost = data?.cost ?? null;
const msg = gain != null
? this.$t('falukant.reputation.actions.success', { gain, cost })
: this.$t('falukant.reputation.actions.successSimple');
this.$root.$refs.messageDialog?.open(this.$t('falukant.reputation.actions.title'), msg);
await Promise.all([this.loadReputation(), this.loadReputationActions()]);
} catch (e) {
const errText = e?.response?.data?.error || e?.message || String(e);
this.$root.$refs.messageDialog?.open(this.$t('falukant.reputation.actions.title'), errText);
} finally {
this.runningActionId = null;
}
},
async loadNobilityTitles() {
this.nobilityTitles = await apiClient.get('/api/falukant/nobility/titels').then(r => r.data)
},
@@ -256,13 +335,14 @@ export default {
},
async mounted() {
const tabFromQuery = this.$route?.query?.tab;
if (['overview','party'].includes(tabFromQuery)) {
if (['overview','party','actions'].includes(tabFromQuery)) {
this.activeTab = tabFromQuery;
}
await this.loadPartyTypes();
await this.loadNobilityTitles();
await this.loadParties();
await this.loadReputation();
await this.loadReputationActions();
}
}
</script>
@@ -323,4 +403,9 @@ table th {
border-top: 1px solid #ccc;
margin-top: 1em;
}
.reputation-actions-daily {
margin: 0.5rem 0 1rem;
font-weight: bold;
}
</style>