xMOO object hierarchy!
(the object hierarchy here is more-or-less what I want ultimately out of xMOO.
objects prefixed with $ are part of LambdaCore; objects prefixed with ? are
created but not quite right yet; objects prefixed with # are uncreated, and
their code has not yet been debugged at all.)
+-$Root Object
+-$generic room
| +-generic sound-carrying room
| +-generic detailed room
| | +-?generic region
| | +-?generic vehicle
| | +-#generic region ship
| +-#generic death room
+-$generic exit
| +-#generic sound-carrying exit
| +-generic lockable doored exit
| +-generic fake exit
+-$generic thing
| +-#generic destructable thing
| | +-#generic fungible thing
| | | +-#generic liquid pool <-- refers to a generic liquid
| | | +-#generic coin stack <-- stack of one money type
| | | +-#generic currency <-- stack of all money types in a currency
| | +-?generic wearable thing
| | | +-generic radio
| | | +-#generic recording device
| | +-?generic furniture
| | +-#generic smoking object <-- object with :smoke &c verbs
| | +-#generic preparation device
| | | +-#generic synther <-- synth stuff for forge
| | | +-#generic forge <-- forge metals & synth
| | | +-#generic stove <-- cook stuff
| | | +-#generic sewing machine <-- fabric + patterns?
| | +-#generic radio tape <-- play <tape> on/with <radio>
| | +-#generic herb <-- smokable object
| | +-#generic food <-- anything you can eat
| | +-#generic liquid <-- can drink, need a liquid container to pick up
| | +-#generic lever
| | | +-#generic button
| | | +-#generic pullcord
| | +-#generic light
| | +-#generic burning light
| | +-#generic lighter
| +-$generic container <!- modify to capacity-check
| | +-#generic liquid container <-- can drink from: water bottle, etc
| | +-#generic refilling liquid container <-- fountain, well
| +-#generic altered object <-- object created via a template based on another object
| +-#generic ruined object <-- object created via decay
| +-#generic synthed object <-- object create via :synth
| +-#generic corpse <-- maybe this should be elsewhere, though
+-#generic embodied object
+-#generic actor
| +-#generic region ship attendant
+-<$player hierarchy>
generic destroyable thing
(maybe needs a .ruin_type prop, to determine how it works when it's ruined--
it could vanish, or change to a #generic ruined object (the scraps of %t) or
whatever else. a piece of cake could rot away completely, but a burning torch
would leave behind a burnt-out stick of ash.)
(this also could use a... .condition_display prop. displayed after object name
in inventory. food could have fresh->{no title}->rotten, etc. prop would be a
list that maps .condition values to messages.)
.condition_max number of times the object can decay before it is ruined
.condition current decay state of the object, between 0 and .condition_max
.decay_msg string or list of strings, to display (one, randomly) when the
object decays
.decay_rate time in seconds between decay steps, or ≤ 0 for don't decay
automatically
.ruin_msg string or list of strings, to display (one, randomly) when the
object decays to ruin.
.repair_msg string or list of strings, to display (one, randomly) when the
object is repaired.
.repairable FALSE if you can't repair the object, TRUE otherwise. (maybe
.repair_requirement for an object/key statement that is needed
to repair the thing)
.decay_task task ID of decay task
:_repaired called to do a repair operation
:_decay to decay at .decay_rate, tell its location, and ruin
:_ruin called when object condition falls to 0.
:initialize start decay process when object is created.
@create $thing called "generic destroyable thing",destroy
@prop destroy.condition_max 10
@prop destroy.condition 10
@prop destroy.decay_msg "%T crumbles slightly."
@prop destroy.decay_rate 0
@prop destroy.ruin_msg "%T crumbles to pieces!"
@prop destroy.repair_msg "%T looks good as new!";
@prop destroy.repairable 1
@prop destroy.decay_task 0
@verb destroy:_repaired tnt
@program destroy:_repaired
this.condition = this.condition_max;
repair = typeof(this.repair_msg) == LIST ? this.repair_msg | {this.repair_msg};
check = random(length(repair));
this.location:tell($string_utils:pronoun_sub(repair[check]));
try
kill_task(this.decay_task)
except (ANY)
endtry
if(this.decay_rate > 0)
fork(this.decay_rate/2)
this:_decay();
endfork
endif
.
@verb destroy:_decay tnt
@program destroy:_decay
this.decay_task = task_id();
while(this.condition > 0)
decay = typeof(this.decay_msg) == LIST ? this.decay_msg | {this.decay_msg};
check = random(length(decay));
this.location:tell($string_utils:pronoun_sub(decay[check]));
this.condition = this.condition - 1;
if(this.decay_rate > 0)
suspend(this.decay_rate);
endif
endwhile
this:_ruin();
.
@verb destroy:initialize tnt
@program destroy:initialize
this.condition = this.condition_max;
if(this.decay_rate > 0)
fork(this.decay_rate/2)
this:_decay();
endfork
endif
.
@verb destroy:_ruin tnt
@program destroy:_ruin
"By default, this utterly destroys the object-- we're assuming the 'original' object will be set +f or have children (or have verb customization), so to avoid getting your objects ruined accidentally, um, set them +f. You'll still lose the object in VR terms (it'll be at #-1, most likely), but it will still /EXIST/.";
if (caller() != this && caller_perms() != $code_utils:verb_perms())
return E_PERM;
endif
ruin = typeof(this.ruin_msg) == LIST ? this.ruin_msg | {this.ruin_msg};
check = random(length(ruin))
this.location:tell($string_utils:pronoun_sub(ruin[check]));
if (this.f || children(this) || verbs(this))
this:moveto(#-1);
return E_NACC;
endif
return $recycler:_recycle(this);
.
generic fungible thing
(needs a take/get override, to properly merge stacks. needs a drop/throw
override, to properly split stacks. both need to understand "(verb) five/5
coins [from container]")
(the get/take override would have to be in a :huh verb, 'cause the parser by
default won't match 'five coins' to an object w/ a coin/coins alias-- also, I
don't think there's a built-in converse to the $string_utils:engligh_number
verb, so I'd probably have to write my own 'twenty-five' => 25 verb.)
(syntax guesses-- if there's no amount modifier and the get/drop is for the
singular form [coin], do action for one unit. if it's for a plural or the
form is collective [water], do action for all units)
(needs general :_split, :_merge, :_spawn, :_remove verbs)
:title tnt needs a hack to display .in_stack after if .in_stack ≥ 2
.stack_limit max. amount of unit which can be in one stack
.in_stack number of units in stack right now
@create $destroyable called "generic fungible thing",fungible
@prop fungible.stack_limit 0
@prop fungible.in_stack 1
@program fungible:title
return pass(@args) + this.in_stack > 1 ? " (", this.in_stack, ")" | "";
.
generic smoking object
:smoke this none none take a drag
:light this none none light (coldstart)
:light this with any light (with match)
:tamp this none none tamp down material
:fill this with any fill with burnable object
:empty this none none empty out, salvaging any burnable object left
.coldstart 0 if needs to be lit with some other burning object
@create $destroyable called "generic smoking object",smoke
@prop smoke.coldstart 1 rc
@verb smoke:smoke this none none
@verb smoke:light this none none
@verb smoke:light this with any
generic burning light
:"light burn" this none none (or this with any if flint &c)
:"unlight extinguish" this none none
generic lever
(this should override the $destroyable stuff to make them easy to break [but
not decay automatically])
.flip_msg string, message to announce when the lever state changes
(either push or pull)
.state in {"push", "pull"}
:"push pull" this none none
:_push tnt whatever action should occur when lever is pushed
:_pull tnt whatever action should occur when lever is pulled
@create $destroyable called "generic lever",lever
@prop lever.flip_msg "%N flips %t."
@prop lever.state ""
@verb lever:"push pull" this none none rd
@program lever:push
if (verb == this.state)
player:tell("You can't ", verb, " that any further.");
else
this.location:announce_all($string_utils:pronoun_sub(this.flip_msg));
this.state = verb;
this:("_"+(verb))();
endif
.
@verb lever:_push tnt
@program lever:_push
return;
.
@verb lever:_pull tnt
@program lever:_pull
return;
.
:push this none none
:pull this none none
@create lever called "generic button",button
@set button.flip_msg to "%N pushes %t."
@verb button:pull this none none
@program button:pull
player:tell("You can't ", verb, " that.");
.
@verb button:push this none none
@program button:push
this.location:announce_all($string_utils:pronoun_sub(this.flip_msg));
this:_push();
.
generic pullcord
:push this none none
:pull this none none
@create lever called "generic pullcord",pullcord
@set pullcord.flip_msg to "%N pulls %t."
@verb pullcord:push this none none
@program pullcord:push
player:tell("You can't ", verb, " that.");
.
@verb pullcord:pull this none none
@program pullcord:pull
this.location:announce_all($string_utils:pronoun_sub(this.flip_msg));
this:_pull();
.
generic tape
(uses for tapes:
- (manually) write a few messages to play on a radio channel
- (with recording device) record messages in a certain room
:play this with/on any (where 'any' is a radio)
:remove this from any (where 'any' is a radio or recording device)
(overload take/get to do :remove in that case, too)
generic preparation device
(children should add additional verbs that expand mixing functionality)
:combine any with any
sees if dobj + iobj makes a valid combo
generic embodied object
(body parts offhand: upper body, neck, head, horn, eye, ear, mouth/lip,
tongue, nose, back, wing, shoulder, upper arm, elbow, forearm, wrist,
hand/paw, finger, abdomen, hips, groin, upper leg, knee, lower leg, ankle,
foot, toe, tail, paw, haunch, tentacle, fin, hoof, foreleg, nail/claw, tusk,
shell, pseudopod, mandible, gill, beak.
'organs': skin, bone, muscle, heart, lung, brain, intestines, liver, spleen,
stomach.
each of those parts could be covered w/ various objects-- most specifically,
'skin' is the basic lowest layer [and can be changed via magic &c],
followed by tattoos, then piercings. all the rest up would be clothing
layers)
not sure what the point of organs would be-- perhaps super-lowermost layers
on various bodyparts?
.parts {{"part name", {layer[, ...]}}, [{part, {layer[, ...]}]}}
.body complex part list
:use_sense tnt {STR sense, STR sense_msg, STR fail_distort, ?begin = "", ?end = ""}
:_sense tnt {STR sense, INT difficulty}
@create #1 called "generic embodied object",embodied,bodied
generic vehicle
(this needs better handling of being able to look outside the vehicle. maybe
default 'look out' verb? also, needs a way to transmit messages [say, emote,
etc] to vehicle location.)
:"enter board embark" this none none
:"exit leave out disembark" any none none
:"drive" any any any rxd
:description tnt override to display location at end
:tell tnt preface all lines w/ "> "
@create $detailed_room called "generic vehicle",vehicle
@verb vehicle:"enter board embark" this none none
@verb vehicle:"ex*it leave o*ut disembark" any none none
@verb vehicle:drive any any any rxd
@verb vehicle:description tnt rxd
@verb vehicle:tell tnt
@prop vehicle.preface "> "
@prop vehicle.location_msg "> Outside, you see %l."
@prop vehicle.player_enter_msg "%N %<climbs> aboard %t."
@prop vehicle.player_exit_msg "%N %<disembarks> from %t."
@program vehicle:enter
if (player.location == this)
player:tell("You're already in there!");
elseif (player.location != this.location)
player:tell("There's nothing like that here.");
elseif (!this:acceptable(player))
"again, a job for nogo_msg";
player:tell("You can't go in ", this:title(), ".");
else
$you:say_action(this.player_enter_msg, player, this, this.location);
player:moveto(this);
suspend(0);
if(player.location == this)
this:announce($string_utils:pronoun_sub(this.player_enter_msg, player, this, this));
endif
endif
.
@program vehicle:exit
if (player.location != this)
player:tell("You're not inside that.");
elseif (!this.location:acceptable(player))
"this looks like a job for nogo_msg";
player:tell("You can't go to ", this.location:title(), ".");
else
$you:say_action(this.player_exit_msg, player, this, this);
player:moveto(this.location);
old_room = this.location;
suspend(0);
if(player.location == old_room)
old_room:announce($string_utils:pronoun_sub(this.player_exit_msg, player, this, old_room));
endif
endif
.
@program vehicle:drive
"This mimics almost exactly $room:go.";
if (!args || !(dir = args[1]))
player:tell("You need to specify a direction");
return E_INVARG;
elseif (valid(exit = this.location:match_exit(dir)))
exit:move(this);
new_room = this.location;
suspend(0);
this:tell(this.location:title());
this:tell_lines(this.location:description());
if (length(args) > 1 && this.location == new_room)
this:go(@listdelete(args, 1));
endif
elseif (exit == $failed_match)
player:tell("You can't go that way (", dir, ").");
else
player:tell("There are several ways which could be called '", dir, "' here.");
endif
.
@program vehicle:description
desc = pass(@args);
loc = $string_utils:pronoun_sub(this.location_msg, this, this, this.location);
if (typeof(desc) == LIST)
return {@desc, loc};
elseif (`length(desc) ! E_TYPE => 0')
return {desc, loc};
else
return loc;
endif
.
@program vehicle:tell
this:announce_all(tostr(this.preface, @args));
pass(@args);
.
generic region ship
(the idea here is that a region ship will be connected to various terminals
[which have various attendants] and those terminals will send itinerary
messages to the region ship, which are either appended to the end of the
itinerary or flagged differently if the stops are already in the itinerary.
then, the region ship will fly around [this:moveto($region_space);
suspend(10); this:moveto(target); suspend(10); etc] taking on passengers
[who can miss their flight] and kicking off passengers [if they don't leave
when by the time the ship starts up again] as they travel.)
(this also needs some good announce verbs-- first thought is between inner
rooms and rooms w/ windows, perhaps also between inner rooms and 'secret'
rooms [ductwork, engineering corridors, etc]-- but in any case, this needs
the capacity to send a message to every room inside the ship, even secret
ones. this is a problem, 'cause :_search_rooms is specifically assumed to not
search secret rooms, but without a listing of secret rooms /somewhere/,
there's no way to announce to them about ship events.)
:drive override to do nothing
:enter override to only allow objects in .toboard in.
.ports list of all ports this ship services. displayed as the
region name, unless the port is not part of a region,
in which case the room name is displayed. (...i think?)
.base_port the room to return to when no trips are queued.
.itinerary {} list of targets to visit.
.rooms {} list of rooms 'inside' ship, starts as {}, use
:_search_rooms to set
:_search_rooms tnt starts at this, follows this.exits until no more rooms
in queue (ignoring any rooms that are in a region)
.passengers {} characters that have boarded that have not yet unboarded
.toboard {} characters that have queued trips but are not yet on the ship
:_add_itin (loc, dest, character to board) -- if loc and dest are
already on itin in proper order, just add character to
the board/leave lists. otherwise, tack on loc & dest
to the end of the itinerary.
:on_ship (obj or list of objs) -- returns list of objs from args
that are findable on the ship.
:travel tnt (the actual moving-about verb)
.travel_id task ID of the travel task, or 0
.kickoff_msg message displayed when a player is kicked off the ship
.missed_msg message told to player when they miss their ship
.flight_time int, time to spend in region space
.dive_time int, time to spend diving into/out of regions
.port_time int, time to spend waiting at a port
-> these should be on the attendant
:charter any to any charter <ship> to <port>
:itin*erary any none none itin <ship>
:schedules none none none tells current info about all ships connected to port
@create $vehicle called "generic region ship","region ship",ship
@prop ship.itinerary {} r
@prop ship.rooms {} r
@prop ship.ports {} r
@prop ship.base_port #-1 rc
@prop ship.passengers {} r
@prop ship.toboard {} r
@prop ship.kickoff_msg "%N %<is> thrown off the ship." rc
@prop ship.missed_msg "You just missed your flight on %t!" rc
@prop ship.flight_time 10 rc
@prop ship.dive_time 2 rc
@prop ship.port_time 5 rc
@verb ship:drive any any any rxd
@verb ship:"enter board embark" this none none
@verb ship:_search_rooms tnt
@verb ship:_add_itin*erary tnt
@verb ship:on_ship tnt
@verb ship:travel tnt
@program ship:drive
"region ships are AUTOMATED. automation is the wave of the future.";
player:tell("I don't understand that.");
.
@program ship:enter
if (!(player in this.toboard))
player:tell("You can't get on ", this:title(), "; you don't have a trip scheduled.");
else
pass(@args);
if (player.location == this)
this.passengers = setadd(this.passengers, player);
this.toboard = setremove(this.toboard, player);
endif
endif
.
@program ship:_search_rooms
"I have worries about this taking forever on large ships.";
visited = connected = {};
queue = {this};
while (length(queue))
node = queue[1];
$command_utils:suspend_if_needed(2);
queue = setremove(queue, node);
if (node in visited || $object_utils:isa(node, $region))
continue;
endif
visited = setadd(visited, node);
for exit in (`node.exits ! E_PERM => node:obvious_exits()')
try
if (!(`exit.dest ! ANY => #-1' in visited))
queue = setadd(queue, exit.dest);
endif
except (ANY)
"...ignore bad exits...";
endtry
endfor
connected = setadd(connected, node);
endwhile
this.rooms = connected;
.
@program ship:_add_itin
{pickup, dropoff, character} = args;
if (pindex = $list_utils:iassoc(this.itinerary, pickup))
this.itinerary[pindex][2] = setadd(this.itinerary[pindex][2], character);
if (dindex = $list_utils:iassoc(this.itinerary, dropoff))
this.itinerary[dindex][3] = setadd(this.itinerary[dindex][3], character);
else
this.itinerary = {@this.itinerary, {dropoff, {}, {character}}};
dindex = length(this.itinerary)-1;
endif
else
this.itinerary = {@this.itinerary, {pickup, {character}, {}}, {dropoff, {}, {character}}};
pindex = length(itinerary)-2;
dindex = length(itinerary)-1;
endif
if(this.travel_id == 0)
fork(0)
this:travel();
endfork
endif
"that's time in seconds til ship is at pickup, dropoff";
return {pindex * (this.flight_time + this.port_time + (this.dive_time * 2)), dindex * (this.flight_time + this.port_time + (this.dive_time * 2))};
.
@program ship:on_ship
":on_ship(OBJ or {OBJS})";
"";
"Returns a list of any objs from list that are in searchable ship rooms. Returns the empty list if no objects found.";
objs = typeof(args[1]) == LIST ? objs | {objs};
found = {};
if (this.rooms == {})
this:_search_rooms();
endif
for obj in (objs)
if (obj.location in this.rooms)
found = {@found, obj};
objs = setremove(objs, obj);
endif
endfor
return found;
.
@program ship:travel
"todo: work on end of verb so travel never stalls (also maybe forking so the task doesn't die if there are :moveto errors?)";
this.travel_id = task_id();
suspend(this.port_time);
this:moveto(this.base_port.region[1]);
suspend(this.dive_time);
this:moveto(`$region_space ! E_PROPNF => #-1');
while (length(this.itinerary))
{target, ?toboard = {}, ?toleave = {}} = this.itinerary[1];
this.itinerary = this.itinerary[2..$];
suspend(this.flight_time);
this:moveto(target.region[1]);
suspend(this.dive_time);
this:moveto(target);
this.toboard = toboard;
suspend(this.port_time);
"kick off anyone scheduled to get off on this stop who isn't hidden";
if (stragglers = this:on_ship(toleave))
for p in (stragglers)
this.passengers = setremove(this.passengers, p);
$you:say_action(this.kickoff_msg, player, this);
try
p:moveto(this.location);
suspend(0);
except (ANY)
endtry
if(p.location == this.location)
this.location:announce($string_utils:pronoun_sub(this.player_exit_msg, player, this, this.location));
endif
endfor
endif
for p in (this.toboard)
try
p:tell($string_utils:pronoun_sub(this.missed_msg));
except (ANY)
entry
endfor
this:moveto(`$region_space ! E_PROPNF => #-1');
endwhile
suspend(this.port_time);
this:moveto(this.base_port.region[1]);
suspend(this.dive_time);
this:moveto(this.base_port);
this.travel_id = 0;
return;
.