421 lines
15 KiB
C
421 lines
15 KiB
C
/* NetHack 3.7 write.c $NHDT-Date: 1702023275 2023/12/08 08:14:35 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.41 $ */
|
|
/* NetHack may be freely redistributed. See license for details. */
|
|
|
|
#include "hack.h"
|
|
|
|
staticfn int cost(struct obj *) NONNULLARG1;
|
|
staticfn int write_ok(struct obj *) NO_NNARGS;
|
|
staticfn char *new_book_description(int, char *) NONNULL NONNULLPTRS;
|
|
|
|
/*
|
|
* returns base cost of a scroll or a spellbook
|
|
*/
|
|
staticfn int
|
|
cost(struct obj *otmp)
|
|
{
|
|
if (otmp->oclass == SPBOOK_CLASS)
|
|
return (10 * objects[otmp->otyp].oc_level);
|
|
|
|
switch (otmp->otyp) {
|
|
#ifdef MAIL_STRUCTURES
|
|
case SCR_MAIL:
|
|
return 2;
|
|
#endif
|
|
case SCR_LIGHT:
|
|
case SCR_GOLD_DETECTION:
|
|
case SCR_FOOD_DETECTION:
|
|
case SCR_MAGIC_MAPPING:
|
|
case SCR_AMNESIA:
|
|
case SCR_FIRE:
|
|
case SCR_EARTH:
|
|
return 8;
|
|
case SCR_DESTROY_ARMOR:
|
|
case SCR_CREATE_MONSTER:
|
|
case SCR_PUNISHMENT:
|
|
return 10;
|
|
case SCR_CONFUSE_MONSTER:
|
|
return 12;
|
|
case SCR_IDENTIFY:
|
|
return 14;
|
|
case SCR_ENCHANT_ARMOR:
|
|
case SCR_REMOVE_CURSE:
|
|
case SCR_ENCHANT_WEAPON:
|
|
case SCR_CHARGING:
|
|
return 16;
|
|
case SCR_SCARE_MONSTER:
|
|
case SCR_STINKING_CLOUD:
|
|
case SCR_TAMING:
|
|
case SCR_TELEPORTATION:
|
|
return 20;
|
|
case SCR_GENOCIDE:
|
|
return 30;
|
|
case SCR_BLANK_PAPER:
|
|
default:
|
|
impossible("You can't write such a weird scroll!");
|
|
}
|
|
return 1000;
|
|
}
|
|
|
|
/* getobj callback for object to write on */
|
|
staticfn int
|
|
write_ok(struct obj *obj)
|
|
{
|
|
if (!obj || (obj->oclass != SCROLL_CLASS && obj->oclass != SPBOOK_CLASS))
|
|
return GETOBJ_EXCLUDE;
|
|
|
|
if (obj->otyp == SCR_BLANK_PAPER || obj->otyp == SPE_BLANK_PAPER)
|
|
return GETOBJ_SUGGEST;
|
|
|
|
return GETOBJ_DOWNPLAY;
|
|
}
|
|
|
|
/* write -- applying a magic marker */
|
|
int
|
|
dowrite(struct obj *pen)
|
|
{
|
|
struct obj *paper;
|
|
char namebuf[BUFSZ] = DUMMY, *nm, *bp;
|
|
struct obj *new_obj;
|
|
int basecost, actualcost;
|
|
int curseval;
|
|
char qbuf[QBUFSZ];
|
|
int first, last, i, deferred, deferralchance, real;
|
|
boolean by_descr = FALSE;
|
|
const char *typeword;
|
|
int spell_knowledge;
|
|
|
|
if (nohands(gy.youmonst.data)) {
|
|
You("need hands to be able to write!");
|
|
return ECMD_OK;
|
|
} else if (Glib) {
|
|
pline("%s from your %s.", Tobjnam(pen, "slip"),
|
|
fingers_or_gloves(FALSE));
|
|
dropx(pen);
|
|
return ECMD_TIME;
|
|
}
|
|
|
|
/* get paper to write on */
|
|
paper = getobj("write on", write_ok, GETOBJ_NOFLAGS);
|
|
if (!paper)
|
|
return ECMD_CANCEL;
|
|
/* can't write on a novel (unless/until it's been converted into a blank
|
|
spellbook), but we want messages saying so to avoid "spellbook" */
|
|
typeword = (paper->otyp == SPE_NOVEL) ? "book"
|
|
: (paper->oclass == SPBOOK_CLASS) ? "spellbook"
|
|
: "scroll";
|
|
if (Blind) {
|
|
if (!paper->dknown) {
|
|
You("don't know whether that %s is blank or not.", typeword);
|
|
return ECMD_OK;
|
|
} else if (paper->oclass == SPBOOK_CLASS) {
|
|
/* can't write a magic book while blind */
|
|
pline("%s can't create braille text.",
|
|
upstart(ysimple_name(pen)));
|
|
return ECMD_OK;
|
|
}
|
|
}
|
|
observe_object(paper);
|
|
if (paper->otyp != SCR_BLANK_PAPER && paper->otyp != SPE_BLANK_PAPER) {
|
|
pline("That %s is not blank!", typeword);
|
|
exercise(A_WIS, FALSE);
|
|
return ECMD_TIME;
|
|
}
|
|
makeknown(SCR_BLANK_PAPER);
|
|
|
|
/* what to write */
|
|
Sprintf(qbuf, "What type of %s do you want to write?", typeword);
|
|
getlin(qbuf, namebuf);
|
|
(void) mungspaces(namebuf); /* remove any excess whitespace */
|
|
if (namebuf[0] == '\033' || !namebuf[0])
|
|
return ECMD_TIME;
|
|
nm = namebuf;
|
|
if (!strncmpi(nm, "scroll ", 7))
|
|
nm += 7;
|
|
else if (!strncmpi(nm, "spellbook ", 10))
|
|
nm += 10;
|
|
if (!strncmpi(nm, "of ", 3))
|
|
nm += 3;
|
|
|
|
if ((bp = strstri(nm, " armour")) != 0) {
|
|
memcpy(bp, " armor ", 7);
|
|
(void) mungspaces(bp + 1); /* remove the extra space */
|
|
}
|
|
|
|
deferred = real = 0; /* not any scroll or book */
|
|
deferralchance = 0; /* incremented for each oc_uname match */
|
|
first = svb.bases[(int) paper->oclass];
|
|
last = svb.bases[(int) paper->oclass + 1] - 1;
|
|
/* first loop: look for match with name/description */
|
|
for (i = first; i <= last; i++) {
|
|
/* extra shufflable descr not representing a real object */
|
|
if (!OBJ_NAME(objects[i]))
|
|
continue;
|
|
|
|
if (!strcmpi(OBJ_NAME(objects[i]), nm)) {
|
|
if (objects[i].oc_name_known
|
|
/* spellbooks can only be written by_name, so no need to
|
|
hold out for a 'better' by_descr match */
|
|
|| paper->oclass == SPBOOK_CLASS) {
|
|
goto found;
|
|
} else {
|
|
/* save item in case there are no better by_descr matches */
|
|
real = deferred = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!strcmpi(OBJ_DESCR(objects[i]), nm)) {
|
|
by_descr = TRUE;
|
|
goto found;
|
|
}
|
|
}
|
|
/* second loop: look for match with user-assigned name */
|
|
/* we will get here if 'nm' isn't a real scroll name/descr, or is the name
|
|
* of a real scroll that hasn't been formally IDed. */
|
|
for (i = first; i <= last; i++) {
|
|
/* player might assign same name multiple times and if so,
|
|
we choose one of those matches randomly */
|
|
if (objects[i].oc_uname && !strcmpi(objects[i].oc_uname, nm)
|
|
/* prefer attempting to write the real scroll type if
|
|
the typename clobbers a real scroll and is known to
|
|
be incorrect */
|
|
&& !(real && objects[i].oc_name_known)
|
|
/*
|
|
* First match: chance incremented to 1,
|
|
* !rn2(1) is 1, we remember i;
|
|
* second match: chance incremented to 2,
|
|
* !rn2(2) has 1/2 chance to replace i;
|
|
* third match: chance incremented to 3,
|
|
* !rn2(3) has 1/3 chance to replace i
|
|
* and 2/3 chance to keep previous 50:50
|
|
* choice; so on for higher match counts.
|
|
*/
|
|
&& !rn2(++deferralchance)) {
|
|
deferred = i;
|
|
/* writing by user-assigned name is same as by description:
|
|
fails for books, works for scrolls (having an assigned
|
|
type name guarantees presence on discoveries list) */
|
|
by_descr = TRUE;
|
|
}
|
|
}
|
|
|
|
if (deferred) {
|
|
i = deferred;
|
|
goto found;
|
|
}
|
|
|
|
There("is no such %s!", typeword);
|
|
return ECMD_TIME;
|
|
found:
|
|
|
|
if (i == SCR_BLANK_PAPER || i == SPE_BLANK_PAPER) {
|
|
You_cant("write that!");
|
|
pline("It's obscene!");
|
|
return ECMD_TIME;
|
|
} else if (i == SPE_NOVEL) {
|
|
boolean fanfic = !rn2(3), tearup = !rn2(3);
|
|
|
|
if (!fanfic) {
|
|
You("%s to write the Great Yendorian Novel, but %s inspiration.",
|
|
!tearup ? "prepare" : "try",
|
|
!Hallucination ? "lack" : "have too much");
|
|
} else {
|
|
You("%sproduce really %s fan-fiction.",
|
|
!tearup ? "start to " : "",
|
|
!Hallucination ? "lame" : "awesome");
|
|
}
|
|
if (!tearup) {
|
|
You("give up on the idea.");
|
|
} else {
|
|
You("tear it up.");
|
|
useup(paper);
|
|
}
|
|
return ECMD_TIME;
|
|
} else if (i == SPE_BOOK_OF_THE_DEAD) {
|
|
pline("No mere dungeon adventurer could write that.");
|
|
return ECMD_TIME;
|
|
} else if (by_descr && paper->oclass == SPBOOK_CLASS
|
|
&& !objects[i].oc_name_known) {
|
|
/* can't write unknown spellbooks by description */
|
|
pline("Unfortunately you don't have enough information to go on.");
|
|
return ECMD_TIME;
|
|
}
|
|
|
|
/* KMH, conduct */
|
|
if (!u.uconduct.literate++)
|
|
livelog_printf(LL_CONDUCT,
|
|
"became literate by writing %s", an(typeword));
|
|
|
|
new_obj = mksobj(i, FALSE, FALSE);
|
|
new_obj->bknown = (paper->bknown && pen->bknown);
|
|
|
|
/* shk imposes a flat rate per use, not based on actual charges used */
|
|
check_unpaid(pen);
|
|
|
|
/* see if there's enough ink */
|
|
basecost = cost(new_obj);
|
|
if (pen->spe < basecost / 2) {
|
|
Your("marker is too dry to write that!");
|
|
obfree(new_obj, (struct obj *) 0);
|
|
return ECMD_TIME;
|
|
}
|
|
|
|
/* we're really going to write now, so calculate cost
|
|
*/
|
|
actualcost = rn1(basecost / 2, basecost / 2);
|
|
curseval = bcsign(pen) + bcsign(paper);
|
|
exercise(A_WIS, TRUE);
|
|
/* dry out marker */
|
|
if (pen->spe < actualcost) {
|
|
pen->spe = 0;
|
|
Your("marker dries out!");
|
|
/* scrolls disappear, spellbooks don't */
|
|
if (paper->oclass == SPBOOK_CLASS) {
|
|
pline_The("spellbook is left unfinished and your writing fades.");
|
|
update_inventory(); /* pen charges */
|
|
} else {
|
|
pline_The("scroll is now useless and disappears!");
|
|
useup(paper);
|
|
}
|
|
obfree(new_obj, (struct obj *) 0);
|
|
return ECMD_TIME;
|
|
}
|
|
pen->spe -= actualcost;
|
|
|
|
/*
|
|
* Writing by name requires that the hero knows the scroll or
|
|
* book type. One has previously been read (and its effect
|
|
* was evident) or been ID'd via scroll/spell/throne (or skill
|
|
* for Wizards) and it will be on the discoveries list.
|
|
* Unknown spellbooks can also be written by name if the hero
|
|
* has fresh knowledge of the spell, or if the spell is almost
|
|
* forgotten and the hero is Lucky (with a greater chance than
|
|
* if the spell is unknown or forgotten).
|
|
* (Previous versions allowed scrolls and books to be written
|
|
* by type name if they were on the discoveries list via being
|
|
* given a user-assigned name, even though doing the latter
|
|
* doesn't--and shouldn't--make the actual type become known.)
|
|
*
|
|
* Writing by description requires that the hero knows the
|
|
* description (a scroll's label, that is, since books by_descr
|
|
* are rejected above). This is done by checking to see if a
|
|
* scroll with the same description has been encountered.
|
|
*
|
|
* Normal requirements can be overridden if hero is Lucky.
|
|
*/
|
|
|
|
if (paper->oclass == SPBOOK_CLASS) {
|
|
spell_knowledge = known_spell(new_obj->otyp);
|
|
} else {
|
|
spell_knowledge = spe_Unknown;
|
|
}
|
|
/* if known, then either by-name or by-descr works */
|
|
if (!objects[new_obj->otyp].oc_name_known
|
|
/* else if named, then only by-descr works */
|
|
&& !(by_descr && objects[new_obj->otyp].oc_encountered)
|
|
/* else fresh knowledge of the spell works */
|
|
&& spell_knowledge != spe_Fresh
|
|
/* and Luck might override after previous checks have failed */
|
|
&& rnl(((Role_if(PM_WIZARD) && paper->oclass != SPBOOK_CLASS)
|
|
|| spell_knowledge == spe_GoingStale)
|
|
? 5 : 15)) {
|
|
You("%s to write that.", by_descr ? "fail" : "don't know how");
|
|
/* scrolls disappear, spellbooks don't */
|
|
if (paper->oclass == SPBOOK_CLASS) {
|
|
You(
|
|
"write in your best handwriting: \"My Diary\", but it quickly fades.");
|
|
update_inventory(); /* pen charges */
|
|
} else {
|
|
if (by_descr) {
|
|
Strcpy(namebuf, OBJ_DESCR(objects[new_obj->otyp]));
|
|
wipeout_text(namebuf, (6 + MAXULEV - u.ulevel) / 6, 0);
|
|
} else
|
|
Sprintf(namebuf, "%s was here!", svp.plname);
|
|
You("write \"%s\" and the scroll disappears.", namebuf);
|
|
useup(paper);
|
|
}
|
|
obfree(new_obj, (struct obj *) 0);
|
|
return ECMD_TIME;
|
|
}
|
|
/* can write scrolls when blind, but requires luck too;
|
|
attempts to write books when blind are caught above */
|
|
if (Blind && rnl(3)) {
|
|
/* writing while blind usually fails regardless of
|
|
whether the target scroll is known; even if we
|
|
have passed the write-an-unknown scroll test
|
|
above we can still fail this one, so it's doubly
|
|
hard to write an unknown scroll while blind */
|
|
You("fail to write the scroll correctly and it disappears.");
|
|
useup(paper);
|
|
obfree(new_obj, (struct obj *) 0);
|
|
return ECMD_TIME;
|
|
}
|
|
|
|
/* use up old scroll / spellbook */
|
|
useup(paper);
|
|
|
|
/* success */
|
|
if (new_obj->oclass == SPBOOK_CLASS) {
|
|
/* acknowledge the change in the object's description... */
|
|
pline_The("spellbook warps strangely, then turns %s.",
|
|
new_book_description(new_obj->otyp, namebuf));
|
|
}
|
|
new_obj->blessed = (curseval > 0);
|
|
new_obj->cursed = (curseval < 0);
|
|
#ifdef MAIL_STRUCTURES
|
|
if (new_obj->otyp == SCR_MAIL)
|
|
/* 0: delivered in-game via external event (or randomly for fake mail);
|
|
1: from bones or wishing; 2: written with marker */
|
|
new_obj->spe = 2;
|
|
#endif
|
|
/* unlike alchemy, for example, a successful result yields the
|
|
specifically chosen item so hero recognizes it even if blind;
|
|
the exception is for being lucky writing an undiscovered scroll,
|
|
where the label associated with the type-name isn't known yet;
|
|
but if writing by description, the description is always known */
|
|
new_obj->dknown = FALSE;
|
|
if (objects[new_obj->otyp].oc_name_known || by_descr)
|
|
observe_object(new_obj);
|
|
|
|
new_obj = hold_another_object(new_obj, "Oops! %s out of your grasp!",
|
|
The(aobjnam(new_obj, "slip")),
|
|
(const char *) 0);
|
|
nhUse(new_obj); /* try to avoid complaint about dead assignment */
|
|
return ECMD_TIME;
|
|
}
|
|
|
|
/* most book descriptions refer to cover appearance, so we can issue a
|
|
message for converting a plain book into one of those with something
|
|
like "the spellbook turns red" or "the spellbook turns ragged";
|
|
but some descriptions refer to composition and "the book turns vellum"
|
|
looks funny, so we want to insert "into " prior to such descriptions;
|
|
even that's rather iffy, indicating that such descriptions probably
|
|
ought to be eliminated (especially "cloth"!) */
|
|
staticfn char *
|
|
new_book_description(int booktype, char *outbuf)
|
|
{
|
|
/* subset of description strings from objects.c; if it grows
|
|
much, we may need to add a new flag field to objects[] instead */
|
|
static const char *const compositions[] = {
|
|
"parchment",
|
|
"vellum",
|
|
"cloth",
|
|
#if 0
|
|
"canvas", "hardcover", /* not used */
|
|
"papyrus", /* not applicable--can't be produced via writing */
|
|
#endif /*0*/
|
|
0
|
|
};
|
|
const char *descr, *const *comp_p;
|
|
|
|
descr = OBJ_DESCR(objects[booktype]);
|
|
for (comp_p = compositions; *comp_p; ++comp_p)
|
|
if (!strcmpi(descr, *comp_p))
|
|
break;
|
|
|
|
Sprintf(outbuf, "%s%s", *comp_p ? "into " : "", descr);
|
|
return outbuf;
|
|
}
|
|
|
|
/*write.c*/
|