Compare commits

3 Commits
14 changed files with 581 additions and 2 deletions
+13
View File
@@ -12,7 +12,20 @@
"type": "data", "type": "data",
"uuid": "a06b47ef-e0d9-4154-94c1-9a6bd86e506f", "uuid": "a06b47ef-e0d9-4154-94c1-9a6bd86e506f",
"version": "1.0.0" "version": "1.0.0"
},
{
"description": "Tamagotchi Scripting",
"type": "script",
"uuid": "3dec0970-c9b8-4e64-886a-cdc237cf938d",
"version": "1.0.0",
"entry": "scripts/main.js"
} }
],
"dependencies": [
{
"module_name":"@minecraft/server",
"version":"2.10.0"
}
], ],
"metadata": { "metadata": {
"authors": ["Momoko"], "authors": ["Momoko"],
View File
View File
+21
View File
@@ -0,0 +1,21 @@
/**
* @typedef {object} Region
* @property {string} nameKey
* @property {number} order
* @property {boolean} unlockedAtStart
* @property {number} unlockedCost
*/
/**
* @type {Record<string, Region>}
*/
export const REGIONS = {
lagoon: {nameKey: "tama.region.lagoon.name",
order: 0, unlockedAtStart: true, unlockedCost: 0},
cropcountry: {nameKey: "tama.region.cropcountry.name",
order: 1, unlockedAtStart: false, unlockedCost: 100},
seaside: {nameKey: "tama.region.seaside.name",
order: 2, unlockedAtStart: false, unlockedCost: 500},
skyland: {nameKey: "tama.region.skyland.name",
order: 3, unlockedAtStart: false, unlockedCost: 1000}
};
View File
+249
View File
@@ -0,0 +1,249 @@
import {
CommandPermissionLevel,
CustomCommandStatus,
CustomCommandParamType,
system,
} from "@minecraft/server";
import { REGIONS } from "../config/regions.js";
import { resetGates, resetGate, unlockGate, unlockGates,
updateBalance, returnBalance, printGateState} from "../save/state.js";
import { unlockRegion } from "../systems/progression.js";
//the system calls this function we do not
//our import into main runs the first line of this etc
system.beforeEvents.startup.subscribe(({ customCommandRegistry }) => {
customCommandRegistry.registerEnum("rzb_tamagotchi:region", Object.keys(REGIONS));
customCommandRegistry.registerCommand(
{
name: "rzb_tamagotchi:print_gates_states",
description: "This command prints the gate states",
permissionLevel: CommandPermissionLevel.Host,
cheatsRequired: true,
},
() => {
system.run(() => {
printGateState();
});
return {
status: CustomCommandStatus.Success
};
}
);
customCommandRegistry.registerCommand(
{
name: "rzb_tamagotchi:reset_gates_all",
description: "This command resets all gates to default",
permissionLevel: CommandPermissionLevel.Host,
cheatsRequired: true,
},
() => {
system.run(() => {
resetGates();
});
return {
status: CustomCommandStatus.Success,
message: "Gates reset to default",
};
}
);
customCommandRegistry.registerCommand(
{
name: "rzb_tamagotchi:reset_gate",
description: "This command resets a single gate",
permissionLevel: CommandPermissionLevel.Host,
cheatsRequired: true,
mandatoryParameters: [
{
// Use the enum by setting the name to the enum name
name: "rzb_tamagotchi:region",
type: CustomCommandParamType.Enum,
},
],
},
(origin, region) => {
system.run(() => {
resetGate(region);
});
return {
status: CustomCommandStatus.Success,
message: "Gate ${REGION[id]} reset to default",
};
});
customCommandRegistry.registerCommand(
{
name: "rzb_tamagotchi:unlocks_gates_all",
description: "This command unlocks all gates",
permissionLevel: CommandPermissionLevel.Host,
cheatsRequired: true,
},
() => {
system.run(() => {
unlockGates();
});
return {
status: CustomCommandStatus.Success,
message: "All gates Unlocked",
};
}
);
customCommandRegistry.registerCommand(
{
name: "rzb_tamagotchi:unlock_gate",
description: "This command unlocks a single gate",
permissionLevel: CommandPermissionLevel.Host,
cheatsRequired: true,
mandatoryParameters: [
{
// Use the enum by setting the name to the enum name
name: "rzb_tamagotchi:region",
type: CustomCommandParamType.Enum,
},
],
},
(origin, region) => {
system.run(() => {
unlockGate(region, true);
});
return {
status: CustomCommandStatus.Success,
message: `Gate ${REGIONS[id]} unlocked`,
};
}),
customCommandRegistry.registerCommand(
{
name: "rzb_tamagotchi:request_unlock",
description: "This command calls unlockRegion(), for entities",
permissionLevel: CommandPermissionLevel.Any,
cheatsRequired: false,
mandatoryParameters: [
{
// Use the enum by setting the name to the enum name
name: "rzb_tamagotchi:region",
type: CustomCommandParamType.Enum,
},
],
},
(origin, region) => {
system.run(() => {
const result = unlockRegion(region);
// no entity to talk to when run from a command block or console
if (!origin.sourceEntity) { return; }
if (result.ok) {
origin.sourceEntity.sendMessage({
translate: "tama.unlock.success",
with: { rawtext: [{ translate: REGIONS[region].nameKey }] }
});
return;
}
// reason codes are turned into player-facing text here.
// Every branch produces a RawMessage - a plain string would be
// sent literally instead of being looked up in the lang file.
let why;
switch (result.reason) {
case "already_unlocked":
why = {
translate: "tama.unlock.already_unlocked",
with: { rawtext: [{ translate: REGIONS[region].nameKey }] }
};
break;
case "missing_prior_region":
why = result.requires
? {
translate: "tama.unlock.missing_prior",
with: { rawtext: [{ translate: REGIONS[result.requires].nameKey }] }
}
: { translate: "tama.unlock.first_region" };
break;
case "insufficient_funds":
why = {
translate: "tama.unlock.insufficient_funds",
with: [String(result.cost), String(result.balance)]
};
break;
case "unknown_region":
why = { translate: "tama.unlock.unknown_region" };
break;
default:
why = {
translate: "tama.unlock.default",
with: { rawtext: [{ translate: REGIONS[region].nameKey }] }
};
}
origin.sourceEntity.sendMessage(why);
});
// note: CustomCommandResult.message is typed `string`, so it cannot
// carry a RawMessage and can never be translated. All player-facing
// text for this command goes through sendMessage above instead.
return {
status: CustomCommandStatus.Success,
};
}),
customCommandRegistry.registerCommand(
{
name: "rzb_tamagotchi:balance_update",
description: "This command updates the player balance",
permissionLevel: CommandPermissionLevel.Host,
cheatsRequired: true,
mandatoryParameters: [
{
name: "rzb_tamagotchi:cost",
type: CustomCommandParamType.Integer,
},
],
},
(origin, cost) => {
system.run(() => { //deferred tick so we get return message first
updateBalance(cost);
// the new balance only exists after the deferred update runs,
// so report it here rather than in the return value below
if (origin.sourceEntity) {
origin.sourceEntity.sendMessage(`Your balance is ${returnBalance()}`);
}
});
return {
status: CustomCommandStatus.Success,
message: `Balance changed by ${cost}`,
};
}
)})
+7
View File
@@ -0,0 +1,7 @@
const DEBUG = true;;
const PREFIX = "[TAMA]";
export function log(message) {
if(!DEBUG) return;
console.warn(`${PREFIX} : ${message}`);
}
View File
+25
View File
@@ -0,0 +1,25 @@
import { log } from "./core/log.js";
import { load } from "./save/state.js";
import { getState } from "./save/state.js";
import "./core/commands.js"
import {world} from "@minecraft/server";
log("main.js loaded)");
/*
Dynamic properties have to exist post worldLoad
1.9 -> 2.0 scripts now load before world load
Grabbed data will throw an exception
if we don't do things after worldLoad
You can't serve a pizza if you don't have a pizza
Players, entities, blocks
Time of day, Gamemode, difficulty
*/
world.afterEvents.worldLoad.subscribe(() => {
load();
console.log(JSON.stringify(getState()));
});
//good for debugging gate values
//console.log(JSON.stringify(getState()));
+147
View File
@@ -0,0 +1,147 @@
import { REGIONS } from "../config/regions.js";
import { log } from "../core/log.js";
import { world } from "@minecraft/server";
const KEY = "tama.progress";
const VERSION = 1; //For updating the json in the save
/**
* The in-memory copy of the whole save document, so we don't re-read the
* world's dynamic properties every time something needs a value.
* Null until load() runs. saveState() writes it back out to the world save.
*
* cache = {
* version: 1,
* coins: 0,
* gates: {
* lagoon: true,
* farmland: false,
* seaside: false,
* skyland: false
* }
* }
*/
let cache = null;
/**
* Reads the save document out of the world and puts it in `cache`.
*
* Nothing stored under KEY yet means a brand-new world - getDynamicProperty
* returns undefined - so build the defaults and write them straight back,
* which is what creates the save.
*
* Otherwise the stored value is JSON text, so parse it back into an object.
*/
export function load(){
const raw = world.getDynamicProperty(KEY);
//let raw = undefined;
//good for debugging before the value is updated
//console.log(raw);
if(raw === undefined) {
cache = buildDefaults();
saveState();
} else {
//good for debugging post update
//console.log(raw);
cache = JSON.parse(raw);
}
if(cache.version < VERSION) {
//if(version < (VERSION) -> add attribute etc))
log("==================================");
log(`version: ${cache.version}, code expects ${VERSION}`);
log("==================================");
//if attribute === undefined -> set default value
cache.version = VERSION;
}
}
/**
* Builds a fresh save document for a world that has never been played.
*
* `gates` is an object (not an array) filled by looping REGIONS and copying
* each region's unlockedAtStart value, so adding a region to the config
* automatically adds its gate here with no change to this function.
*/
function buildDefaults() {
const gates = {};
const coins = 0;
for(const id in REGIONS)
{
gates[id] = REGIONS[id].unlockedAtStart;
}
return{version: VERSION, gates: gates, coins: coins};
}
/**
* Returns the cached save document.
* This is the live object, not a copy - anything that mutates what comes back
* must call saveState() afterwards, or the change is lost on world close.
*/
export function getState() {
return cache;
}
/**
* buildDefaults returns a value,
* using .gates returns just the
* gates value rather than coins
* and other default data
*/
export function resetGates() {
cache.gates = buildDefaults().gates;
saveState();
}
export function resetGate(id) {
//resets the gate from the config/region config
cache.gates[id] = REGIONS[id].unlockedAtStart;
saveState();
}
/**
* Sets a region's gate to T/F and persists the change.
* e.g. updateGate("farmland", state) ->
* cache.gates["farmland"] = state
*
* undefined will remove a gate from the cache
*/
export function unlockGate(nKey, state) {
cache.gates[nKey] = state;
saveState();
}
export function unlockGates() {
for(const id in REGIONS) {
cache.gates[id] = true;
}
saveState();
}
export function updateBalance(cost) {
cache.coins += cost;
saveState();
}
export function returnBalance(){
const value = cache.coins;
return value;
}
export function printGateState(){
log(JSON.stringify(getState().gates));
}
/**
* Writes the cache out to the world save, where it survives the world closing.
*
* Returns early if nothing has been loaded yet - writing a null cache would
* store the text "null" and corrupt the save.
*
* Objects are not a permitted dynamic property value (only boolean, number,
* string and Vector3), so the document is converted to JSON text first.
*/
export function saveState() {
if(cache === null) return;
const text = JSON.stringify(cache);
world.setDynamicProperty(KEY, text);
}
+104
View File
@@ -0,0 +1,104 @@
import { REGIONS } from "../config/regions.js"
import { getState, updateBalance, unlockGate, returnBalance } from "../save/state.js"
/**
* Can this region be unlocked right now, and if not, why not?
*
* Returns an outcome object rather than a boolean so callers can explain the
* refusal to the player. `reason` is a code, not display text - whoever shows
* it is responsible for turning it into a translated message.
*
* { ok: true }
* { ok: false, reason: "unknown_region" } developer error
* { ok: false, reason: "already_unlocked" }
* { ok: false, reason: "missing_prior_region", requires: "farmland" }
* { ok: false, reason: "insufficient_funds", cost: 500, balance: 120 }
*
* @param {string} regionID - a REGIONS key, e.g. "farmland"
*/
export function canUnlock(regionID) {
/*
the region's whole config record:
{ nameKey, order, unlockedAtStart, unlockedCost}
*/
const region = REGIONS[regionID];
if (region === undefined) {
return { ok: false, reason: "unknown_region" };
}
// order number from the REGIONS section we keyed
const order = region.order;
// we return the full record.
const gates = getState().gates;
/*
if the records value for that key's order is
true: unlocked
*/
if (gates[regionID] === true) {
return { ok: false, reason: "already_unlocked" };
}
/*
WE ARE KEYING FOR THE KEY VALUE'S ORDER PRIOR TO PASSED REGION{RegionID}
the prior value has to be true for the passed region to unlock
*/
let priorRegion = null;
for (const id in REGIONS) {
// never pass lagoon otherwise all conditions fail
if (REGIONS[id].order === order - 1) {
/*
again tdlr we get the key prior to the past key
so for example command canUnlock("seaside");
we are retrieving farm land for comparison
it's sequential you can't unlock zones non
out of order
*/
priorRegion = id;
}
}
// if first region is auto unlocked we will never meet this condition
if (priorRegion === null) {
return { ok: false, reason: "missing_prior_region", requires: null };
}
/*
the prior region before this one must be open first
*/
if (gates[priorRegion] !== true) {
return { ok: false, reason: "missing_prior_region", requires: priorRegion };
}
//they have to be able to pay for it.
const cost = region.unlockedCost;
const balance = returnBalance();
if (balance < cost) {
return { ok: false, reason: "insufficient_funds", cost: cost, balance: balance };
}
return { ok: true };
}
/**
* Unlocks a region if the rules allow it, charges the cost, and saves.
* object canUnlock produced and can explain what went wrong.
*
* @param {string} regionID - a REGIONS key, e.g. "farmland"
*/
export function unlockRegion(regionID) {
const result = canUnlock(regionID);
if (!result.ok) {
return result;
}
unlockGate(regionID, true);
/*
always make sure things that cost money are passed negative,
because otherwise it would add to the balance
*/
updateBalance(-REGIONS[regionID].unlockedCost);
return { ok: true };
}
+15
View File
@@ -0,0 +1,15 @@
pack.name=The Tamagotchi Resource Pack
pack.description=A Resource pack for the Tamagotchi Map
tama.region.lagoon.name=Lagoon Town
tama.region.cropcountry.name=Crop Country
tama.region.seaside.name=Seaside
tama.region.skyland.name=Skyland
tama.unlock.already_unlocked=%1 is already unlocked
tama.unlock.missing_prior=You must unlock %1 first
tama.unlock.insufficient_funds=You need %1 coins, you have %2
tama.unlock.first_region=First region is already unlocked
tama.unlock.unknown_region=Requested region not found
tama.unlock.default=Cannot unlock Region: %1
tama.unlock.success=%1 unlocked
-2
View File
@@ -1,2 +0,0 @@
pack.name=The Tamagotchi Resource Pack
pack.description=A Resource pack for the Tamagotchi Map