Compare commits
3
Commits
main
...
tamagotchi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e17acc4cf | ||
|
|
8fbd3bd527 | ||
|
|
9dad3c6176 |
@@ -12,6 +12,19 @@
|
||||
"type": "data",
|
||||
"uuid": "a06b47ef-e0d9-4154-94c1-9a6bd86e506f",
|
||||
"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": {
|
||||
|
||||
@@ -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}
|
||||
};
|
||||
@@ -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}`,
|
||||
};
|
||||
}
|
||||
)})
|
||||
@@ -0,0 +1,7 @@
|
||||
const DEBUG = true;;
|
||||
const PREFIX = "[TAMA]";
|
||||
|
||||
export function log(message) {
|
||||
if(!DEBUG) return;
|
||||
console.warn(`${PREFIX} : ${message}`);
|
||||
}
|
||||
@@ -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()));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,2 +0,0 @@
|
||||
pack.name=The Tamagotchi Resource Pack
|
||||
pack.description=A Resource pack for the Tamagotchi Map
|
||||
Reference in New Issue
Block a user