User Tools

Site Tools


sd:rogueima_plan

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
sd:rogueima_plan [2026/09/08 07:57] – external edit 127.0.0.1sd:rogueima_plan [2026/09/09 03:27] (current) – external edit 127.0.0.1
Line 850: Line 850:
 ''S'' to save and resume. The file services already exist behind ''INT $15''. ''S'' to save and resume. The file services already exist behind ''INT $15''.
 A high score table after that. A high score table after that.
 +
 +===== Part VII — saying it properly =====
 +
 +==== Step 21. Item names in messages - DONE ====
 +
 +Everything says //"You wield it."// The item knows its name; the message does
 +not ask. NetWhack has a small naming layer that every message goes through:
 +
 +  * ''name()''     — the real name, with ''pre_name'' and ''post_name'' when identified
 +  * ''aname()''    — //"a dagger"//, //"an athame"//
 +  * ''tname()''    — //"the dagger"//
 +  * ''bcstatus()'' — //"blessed "// / //"cursed "// / //"uncursed "//, when known
 +  * ''msg.You(s)'' — prints //"You "// + s
 +
 +so ''msg.You("wield " + tname())'' is //"You wield the dagger."// We already
 +build names for potions and scrolls; this generalises that and routes the
 +messages through it.
 +
 +**Do this first.** It is small, it is the difference between a prototype and a
 +game every single turn, and everything below it wants to say something.
 +
 +The message window is 19 columns and wraps mid-word, so this also wants a
 +word-wrapping ''print_msg'' — the hand-broken strings do not survive a name
 +being spliced into the middle of them.
 +
 +===== Part VIII — things that do something =====
 +
 +==== Step 22. Effects, as three things instead of one - DONE ====
 +
 +=== Why not NetWhack's Effect class ===
 +
 +''Effect.java'' is the **only major system in NetWhack that is not a table**.
 +There is an ''ArmorData'', a ''MobData'', a ''PotionData'', a ''TileData'' and
 +a ''WeaponData''. There is no ''EffectData''. Instead there is one class with
 +twelve fields --
 +
 +  ticks  uses  trigger  type  expired  index  value  extra
 +  parent  item  mobile  engine
 +
 +-- of which three (''index'', ''value'', ''extra'') are untyped registers
 +whose meaning changes with the type, and **four separate ''switch (type)''
 +statements** in the same file: ''on_transfer'', ''process'', ''on_remove'' and
 +''changekind''. One effect's behaviour is smeared across four places, and
 +adding one means editing all four.
 +
 +It is also inconsistent with itself about the same job. ''E_E_MODSTAT'' is
 +computed on read -- its ''on_transfer'' and ''on_remove'' are empty, with a
 +comment saying it is handled in ''Mobile.get_attribute()''. ''E_E_DEFENSE'',
 +three cases away, caches: ''eAC += value'' on transfer and ''-= value'' on
 +remove. **Two opposite strategies for the same job in one switch.**
 +
 +A table row tells you when you are finished: the row is full. An object with
 +three general-purpose registers and no schema never does. That is what makes
 +the design feel open-ended -- there is no point at which it says //done//, so
 +every new effect reopens the whole question.
 +
 +=== It was doing three unrelated jobs ===
 +
 +^ NetWhack's ^ what it actually is ^
 +| ''MODSTAT'', ''DEFENSE'', ''PRE_ATP'' | derived state, not an event |
 +| ''SPEED'', ''BLIND'', ''TEMPORAL_SUSPENSION'' | genuinely "for N turns, X" |
 +| ''EDNAS_PIES'' | seven lines of prose -- a cutscene in an effect costume |
 +
 +The pie is the tell. It became an Effect because Effect was the only hook
 +available; the system attracted things that did not belong in it.
 +
 +So the three are split, and each goes where the game already keeps that kind
 +of thing.
 +
 +=== 1. Continuous modifiers: computed, never stored ===
 +
 +''attack_rating'' and ''defense_rating'' already walk the equipment rack
 +adding terms up. A condition is **one more term in that walk**. Nothing is
 +applied and nothing has to be un-applied.
 +
 +That deletes a whole class of bug. ''eAC += value'' / ''eAC -= value'' is
 +wrong for ever if the effect is removed twice, or if the value changes while
 +it is worn, or if a save reorders things. A number computed on read cannot
 +desynchronise. NetHack computes AC from worn armour every time for exactly
 +this reason.
 +
 +=== 2. Timed conditions: a flat array of counters ===
 +
 +''cond.sda''. One slot per kind, holding **turns remaining** -- NetHack's
 +''u.uprops[]''. Setting one is a store, ticking them is one loop in
 +''sched_turn'', expiring one is a compare against zero. No objects, no list,
 +no ''expired'' flag, no ''gc()''.
 +
 +  .equ CD_NAME      ; 3 bytes: what to call it
 +  .equ CD_START  3    ; 3 bytes: what to say when it begins
 +  .equ CD_END    6    ; 3 bytes: ... and when it wears off
 +  .equ CD_SIZE   9
 +
 +  .equ C_BLIND      ; Effect.E_P_BLIND
 +  .equ C_FAST    1    ; Effect.E_P_SPEED
 +
 +That is the schema the Effect class never had, and **a row is finished when
 +those three pointers are filled in**.
 +
 +The API is four routines: ''cond_set(kind, turns)'', ''cond_on(kind)'',
 +''cond_tick()'' and ''cond_slot(kind)''. ''cond_set'' only says the start
 +message when the condition was not already true, so a second potion of speed
 +lengthens the haste rather than announcing it twice.
 +
 +=== Read where it matters, applied nowhere ===
 +
 +This is the part worth keeping hold of. A condition is never pushed into
 +anything -- the one place that cares **asks**:
 +
 +  * **Blindness** is asked about in ''los_update'', which then marks only the
 +    square you stand on. NetWhack does ''mobile.blind++'' on transfer and
 +    ''blind--'' on remove, and has to get both right for ever.
 +  * **Haste** is asked about in ''player_charge'', where an action costs half
 +    the clock. NetWhack's ''E_P_SPEED'' instead does ''action_time -= value''
 +    every turn //from inside the effect// -- the same idea pushed the other
 +    way round, the effect reaching into the mobile rather than the mobile
 +    asking the effect.
 +
 +=== 3. One-shot moments: the item's own routine ===
 +
 +''potion_effect'' in ''food.sda'': a dispatch on kind, like ''mon_damage'' and
 +''item_name'' already are. A potion of healing heals you at the point where
 +you drank it, and does not need to become an object with a lifecycle first.
 +Each potion's whole behaviour is readable in one piece.
 +
 +Nutrition is not in there -- every potion has some and ''do_quaff'' has
 +already applied it, which is why a potion of water does nothing else at all.
 +
 +=== Verification ===
 +
 +  blind on after cond_set        1      turns after 2 of 5      3
 +  blind on after 5 ticks              action, normal       1000
 +  action, hasted               500      healing, 2d6 from 1     9
 +  poison, 2d6 from 20           17      blind from potion     250
 +  speed from potion            100      second potion         200
 +
 +and in the real game, poked blind and stepped: lit floor **194 to 0**, all 195
 +squares still remembered in dark grey, items drawn **2 to 0**. You keep the
 +map you know and see none of it.
 +
 +=== What is left ===
 +
 +''TEMPORAL_SUSPENSION'' and the pie have no slots yet; both are one row and a
 +case when they are wanted. The nine NetWhack effect types are otherwise
 +covered, with less machinery than the Effect class alone.
 +
 +==== Step 23. The speed system - DONE ====
 +
 +''MobInfo'' carries ''mspeed'' and ''aspeed'' and has since Step 18 — a rock
 +mole is 1500, a phase rat 400, the player 1000 — and **nothing reads either**.
 +Every mobile moves once per turn, so a phase rat is exactly as quick as a
 +rock mole.
 +
 +NetWhack's own comment says how it is meant to work: //"relative speed can be
 +added by increasing a speed variable and only allowing movement when it
 +reaches a certain value (then resetting it)"//. An energy counter per mobile,
 +topped up each turn by its speed, and it acts while it can afford to. That is
 +also what turns ''gametime'' into the fine-grained clock its
 +''% TICKS_PER_TURN'' assumes — ours counts player turns because nothing needed
 +finer.
 +
 +This is the one imported field that changes how the game //plays// rather than
 +how it reads, and the potion of speed has nothing to do without it.
 +
 +**Taken before Step 22**, because half of the ''Effect'' triggers --
 +''T_PERTICK'', ''T_PERTURN'', ''T_PERSTEP'' -- have nothing to fire against
 +until a clock exists.
 +
 +=== There is no event queue ===
 +
 +Worth writing down, because it is easy to misremember: **NetWhack has no
 +global event list at all.** No event class, nothing scheduled centrally.
 +''Engine.do_tick(m)'' is
 +
 +  m.per_tick();
 +  m.action_time--;
 +  if (m.action_time > 0) return;   // no energy yet
 +  ...act...
 +  m.action_time += m.movespeed;    // and pay for it
 +
 +per mobile per gametick, and effects live in ''m.fxlist'' on the mobile that
 +owns them, processed by trigger and swept by ''gc()''. Nothing reaches out of
 +an item into the engine. So the counter-per-mobile is kept exactly as it is.
 +
 +=== What is not kept: the ticking ===
 +
 +''do_tick'' runs for every mobile on every one of the thousand gameticks. In
 +Java that is free. Twenty mobiles x a thousand ticks x ten instructions is
 +**200,000 instructions a turn** -- about 154ms at the 1.30M instructions/sec
 +this machine measures at, on top of the 48ms Step 11 already costs, and it
 +grows with the monster count.
 +
 +So time does not advance one unit at a time. The **smallest** counter is
 +found, that much is taken off every counter at once, and whoever reaches zero
 +acts. Identical arithmetic -- 900 still acts more often than 1000 -- for one
 +pass per action instead of a thousand passes per turn. The 1000 scale is kept;
 +the resolution lives in the arithmetic, not in the loop count.
 +
 +=== A field, not a list ===
 +
 +''OBJ_READY'' is a field in each record rather than an entry in a central
 +list, and that is the important choice. A list needs entries removed when a
 +monster dies, a level changes, or a node is recycled -- and a missed removal
 +is a stale event pointing at whatever now occupies that node. **A counter in
 +the record dies with the record.** It is also nearly free: the world queue is
 +already walked every turn by ''move_mon'', ''draw_items'' and
 +''pick_up_items'', so this is one more comparison on a scan that was happening
 +anyway. Only ''OT_MONSTER'' records are scanned; items on the floor do not act.
 +
 +''move_mon'' charges the monster **before** it moves, so every way out of the
 +routine has been paid for. Otherwise one wedged against a wall never advances
 +its counter and the scheduler hands it every turn for ever.
 +
 +A turn is still ''SPD_PLAYER'' units of clock, so hunger, regeneration and the
 +wandering-monster roll fire exactly as often as they did -- a loop rather than
 +an if, since one slow action can cross two turns.
 +
 +=== Verification ===
 +
 +The claim is a ratio, so it is counted rather than looked at. 400 scheduler
 +steps with three actors:
 +
 +  player     (1000)   120 acts
 +  phase rat   (400)   300 acts     = 120 x 1000/400
 +  rock mole  (1500)    80 acts     = 120 x 1000/1500
 +
 +and in the real loop with twenty monsters running, 97 turns left the energy
 +meter at exactly 5000 - 97.
 +
 +''OBJ_LEN'' went 37 to 39 for the counter, so the 4 KB node array holds 105
 +rather than 110.
 +
 +==== Step 24. Potions that do what they say - DONE ====
 +
 +Healing, poison, blindness and speed, all four landed with Step 22, since
 +''potion_effect'' is the shape that step decided on: a dispatch on kind, one
 +case per potion, each readable in one piece.
 +
 +  healing      2d6 back, clamped by heal_player
 +  poison       2d6 off, floored at 0, and -250 nutrition from the table
 +  blindness    C_BLIND for 250 turns
 +  speed        C_FAST for 100 turns
 +  water        nothing at all -- its 100 nutrition is the whole of it
 +
 +Blindness did **not** want ''LOS_RADIUS'' set to 0, as this step guessed
 +before it was written. ''los_update'' asks ''cond_on(C_BLIND)'' and marks only
 +the square you stand on -- a temporarily-modified global would have to be put
 +back, and putting things back is the failure mode Step 22 exists to avoid.
 +
 +==== Step 25. Scrolls, and ''r'' to read - DONE ====
 +
 +''Scroll.event_read'', in the shape Step 22 settled on: a dispatch on kind,
 +one case per scroll, each readable in one piece.
 +
 +^ scroll ^ what it does ^
 +| identify | learn what one thing in your pack is |
 +| teleport | somewhere else on this level |
 +| town portal | says so -- there is no town until Step 30 |
 +| crumpled note | reads it, and does **not** vanish |
 +| temporal suspension | everything else waits 5 to 20 turns |
 +
 +Reading identifies the scroll, so the message uses the name it had **before**
 +you read it: //"You read the scroll of gibberish."// NetWhack only
 +self-identifies the teleport one, inside ''do_teleport'', which looks like an
 +omission rather than a decision -- you plainly learn what a scroll was by
 +watching what it did.
 +
 +=== Temporal suspension is not a condition ===
 +
 +NetWhack does ''action_time -= Dice.roll(5,20) * 1000'', giving the player
 +credit so that everyone else has to wait. Our counters are unsigned and count
 +**down**, so the same thing is expressed from the other side: everybody else
 +is pushed back by that much. Identical in effect, and it cannot go negative.
 +
 +It also **must not** be a condition that makes the player's actions free.
 +''sched_find'' would then return zero for ever, so the clock would never
 +advance, so ''sched_turn'' would never fire, so ''cond_tick'' would never run
 +-- and the suspension would never end. A condition has to be something the
 +clock can outlive.
 +
 +=== A bug inherited and not copied ===
 +
 +''sc_identify'' does what NetWhack's ''do_id'' meant to do. That one builds
 +its list of candidates with
 +
 +  if (i.identified == false);
 +      a_list.add(i);
 +
 +-- a stray semicolon, so the ''add'' is unconditional and the scroll can spend
 +itself telling you about something you already knew.
 +
 +=== Verification ===
 +
 +  unknown kinds in the pack     2 -> 1
 +  teleport moved the player     yes, and to somewhere walkable (3 runs)
 +  clock added by suspension     8000 / 18000 / 11000
 +
 +=== Two hazards of the assembler, found the hard way ===
 +
 +''LDBL AL'' and ''LDAL XL'' are **not** register moves, and the assembler
 +takes both without a word. The first made identify choose nothing; the second
 +stored garbage into ''PX'' and ''PY'', so teleport put the player inside a
 +wall -- and only showed up because the test asked whether the destination was
 +walkable rather than only whether he had moved. ''MOV'' is the register move;
 +''LDxx'' loads an immediate or from memory.
 +
 +==== Step 26. Blessed, cursed, uncursed ====
 +
 +Three bits per item and ''bcstatus()'' in front of the name. Cursed armour
 +that will not come off is the first thing in the game that can go **wrong**
 +in an interesting way.
 +
 +===== Part IX — a world =====
 +
 +==== Step 27. Tiles store their kind - DONE ====
 +
 +A square used to store **the character it looked like**. It stores its
 +**kind** now, and the glyph is one column of a generated table along with the
 +colour, the flags, the name and the description. Same two bytes per square.
 +
 +=== Why this had to come before the village ===
 +
 +''TileData'' draws chair, bridge, bed, road, table and throne **all as ''='''',
 +and wall, secret door and altar **all as ''#''''. A map that stores glyphs
 +cannot tell a bridge from a chair -- so it cannot say whether you may walk on
 +it, what colour to draw it, or what it is called when you look at it.
 +
 +The map SOURCE alphabet is a different thing and is unambiguous: ''r'' road,
 +''B'' shop counter, ''d'' bed, ''f'' flowers, ''b'' bridge, ''='' chair,
 +''s'' secret door, ''*'' water, ''T'' tree. That is why a level can be written
 +as text at all. ''src_to_kind'' is ''DungeonMaker'''s switch, one character to
 +one kind; only the DISPLAY collides.
 +
 +=== The table ===
 +
 +''tiletable.sda'', generated by ''tools/gen_tiletable.py'':
 +
 +  .equ TI_GLYPH  0    ; 1 byte : what it is drawn as
 +  .equ TI_COLOR  1    ; 1 byte : and in what colour
 +  .equ TI_FLAGS  2    ; 1 byte : TF_WALKABLE | TF_OPAQUE
 +  .equ TI_TNAME  3    ; 3 bytes: -> its name, for looking at it
 +  .equ TI_DESC      ; 3 bytes: -> the long description. ON TAP
 +  .equ TI_TSIZE  9
 +
 +The glyph, colour, name and description come from ''TileData''. **The flags do
 +not** -- walkable and vblock are set by a switch in ''Tile.changekind()'' and
 +are not in the table at all. The generator reads both files, so the two cannot
 +drift apart.
 +
 +Two deliberate departures, both made in the generator where they are visible
 +rather than in the data where they would look like the source:
 +
 +  * Four kinds are drawn with **box-drawing characters** and two with a
 +    **space**. Neither survives an ASCII renderer, and a space is what an
 +    unseen square looks like -- a shop floor drawn as one would be invisible.
 +    Those six get stated ASCII stand-ins.
 +  * A **wall is light grey**, not ''TileData'''s ''DARK_GRAY'', which is
 +    exactly the colour a remembered square is drawn in. A secret door matches
 +    the wall it is pretending to be.
 +
 +=== What it cost ===
 +
 +''tile_flags_for'' used to test for ''#'' and ''+'' and call everything else
 +floor -- as far as two glyphs could take it. ''get_glyph'' looks the glyph up;
 +''put_glyph'' became ''put_kind''; ''draw_world'', ''open_door'',
 +''place_stairs'', ''expand_level'' and the whole dungeon maker read and write
 +kinds. ''tiletable.sda'' has to be assembled **before** ''map.sda'', because
 +''.equ'' has no forward references and ''TK_TEMP'' is derived from ''TK_COUNT''.
 +
 +=== And Brynn ===
 +
 +The village is the first level: ''Brynn.java'', **84 x 30**, verbatim but for
 +trimming each row to the 84 columns it declares -- the Java rows are one
 +character longer. ''popfreq'' 0, because nothing wanders into a town.
 +
 +Its well is **not in the map text**; NetWhack adds it in code. Read the code
 +and not the comment above it: the comment says //"Add the well at 24,4"// and
 +the three lines below say ''s.xpos = 4; s.ypos = 28''. **Lower left.** That is
 +the level's ''LV_DOWN''.
 +
 +=== Where does NetWhack put the player? Nowhere ===
 +
 +This is worth writing down because it cannot be found by looking for it.
 +''Level.java'' declares
 +
 +  public int px_last = 0, py_last = 0;
 +
 +and line 519 does ''pc.xpos = px_last''. **Brynn never assigns either.** So the
 +answer is the field initialiser: NetWhack starts you in the top-left corner.
 +Ours starts on the road at **2,6**, which is a decision rather than an
 +accident.
 +
 +That needed a new field. ''LV_UP'' was doing two jobs -- where the stairs up
 +are, and where you appear -- and Brynn has no stairs up, so the two had to
 +come apart. ''LV_START'' is where a new game begins; ''LV_SIZE'' 23 to 25.
 +
 +**0,0 means "no such staircase."** ''place_stairs'' skips one whose
 +coordinates are 0,0, that being the one square no real staircase can occupy --
 +the map corner on any level with a border. This replaced a test on
 +''level_index'', which did not work: ''load_all_levels'' walks the levels with
 +''TL'' and never updated ''level_index'' while building, so the test read a
 +stale value. (It does now.)
 +
 +Checked on the tile array rather than by eye: exactly one staircase on Brynn,
 +kind 7 at (4,28), and no kind 6 anywhere.
 +
 +The 80 x 40 scratch room is gone, and so are the starting dagger, armour and
 +ration -- scaffolding for testing the item system, which the shops will
 +replace. ''x'' still conjures one item from the whole table.
 +
 +Trees draw **green**, water **blue**, roads **brown**, flowers **grey**, each
 +in a lit and a remembered shade. None of that was expressible before.
 +
 +=== What Brynn still has not got ===
 +
 +The map only. The ''DENIZEN'' and ''SHOPKEEPER'' lines that follow it in the
 +Java are Step 28, and the people they describe are Steps 31 and 32. The
 +village is a place; it is not yet inhabited.
 +
 +==== Step 28. A level script, and people who answer questions - DONE ====
 +
 +A level's map text is now followed by directives:
 +
 +  DENIZEN Farmer_Jim 19 17
 +  CHAT  The_apples_look_lovely_this_year.
 +  REPLY job   I_work_the_orchard._Apples,_mostly._It's_a_living.
 +  REPLY name  I'm_Jim._Farmer_Jim,_on_account_of_the_farm.
 +
 +''DENIZEN'', its inline chats and ''NOMONSTERS'' are NetWhack's, read the way
 +''DungeonMaker'' reads them -- split on spaces, ''_'' standing in for a space
 +inside a token -- so **Brynn's own four denizens parse as written**. ''CHAT''
 +and ''REPLY'' are ours.
 +
 +=== Why a directive and not punctuation ===
 +
 +''job:I'm_a_farmer'' reads well and would work. Split on the **first** colon
 +and the answer can contain as many more as it likes, so no escape is needed
 +for the colon at all -- a keyword is one word and cannot contain one.
 +
 +Two things argued against it:
 +
 +  * The format already has **exactly one escape**, ''_'' for space. A
 +    backslash rule would be a second one to learn, and every directive added
 +    later would inherit both.
 +  * Inferring the //kind// of speech from a punctuation mark cannot tell a
 +    keyword from a random line that happens to contain a colon.
 +
 +A directive says which it is instead of leaving it to be guessed, and the next
 +thing we want -- ''GIVE'', ''QUEST'', ''SHOP'' -- is another word rather than
 +another mark.
 +
 +=== Talking: Ultima IV's three cases ===
 +
 +NetWhack has only the middle one. An NPC there holds a list of lines and says
 +one at random; there is no interactive speech in it anywhere. The other two
 +are Rogueima's.
 +
 +^ case ^ what happens ^
 +| nothing to say | //"They do not seem to want to talk."// |
 +| random speech | Farmer Jim says, "The apples look lovely this year." |
 +| keyword replies | a page of its own, and you type words at it |
 +
 +  Farmer Jim -- and you are talking to them.
 +
 +    I work the orchard. Apples, mostly. It's a living.
 +
 +    Ask about a subject -- try NAME, or JOB. BYE to stop.
 +    Say:
 +
 +An unknown word gets //"They shrug, and say nothing about that."//; ''BYE'' or
 +an empty line ends it. Matching folds case, so ''JOB'' and ''job'' are the
 +same question.
 +
 +''npc_mode'' makes the three-way choice **a value rather than a shape of
 +code**, so it can be checked without reading a screen. Replies win over chats:
 +somebody who will hold a conversation should not be reduced to muttering one
 +line at you.
 +
 +=== Nothing copies text ===
 +
 +The script stays in the program image and an NPC holds **pointers into it**.
 +''print_script'' turns the underscores back into spaces on the way out, rather
 +than rewriting the source the way ''replace('_',' ')'' does -- so a level can
 +be built twice without its script having been consumed. A keyword ends at the
 +space that follows it in the script, since it has no terminator of its own.
 +
 +=== Verification ===
 +
 +  denizens created          4        chat lines             21
 +  keyword replies          20        "JOB" finds an answer   yes
 +  "wombat" does not       yes        modes: 2, then 1, then 0
 +
 +The three modes are read off one denizen stripped in stages: with both, it
 +converses; with its replies taken away it falls back to random; with its chats
 +gone too it has nothing to say.
 +
 +=== An ordering bug ===
 +
 +''init_objects'' and ''npc_init'' ran **after** ''load_all_levels'', so the
 +script put four denizens into the node array and ''init_objects'' immediately
 +emptied it again -- ''npc_count'' read 0 and the village was deserted.
 +Everything a level writes into has to exist before the level is built.
 +
 +==== Step 29. Branches, and stairs that know where they go ====
 +
 +The dungeon is not one stack. A staircase carries a **destination branch**:
 +
 +  Brynn --(the well)--> BrynnWell --> LCave
 +                                  --> BDungeon --(depth 9)--> Croky Castle
 +
 +''LevelFactory.createlevel'' takes a branch name and a depth and dispatches on
 +it; ''LevelLibrary'' keeps the levels already built so going back finds them
 +as you left them. Our ''level_table'' is a flat list of five and will need to
 +become that.
 +
 +==== Step 30. The village of Brynn ====
 +
 +Depth 0, ''popfreq'' 0 — no wandering monsters, which is what a town //is//.
 +The well in the middle is a staircase into ''BrynnWell''. Needs Steps 27, 28
 +and 29 first, and then it is mostly data.
 +
 +==== Step 31. NPCs and chat ====
 +
 +''mobile/NPC.java''. A denizen has a name, a position and a list of things to
 +say; ''t'' picks one at random. ''Fortune'' supplies a rumour when the script
 +says ''RANDOM''. Rufus the dog lives here too — the first mobile that is
 +neither the player nor an enemy.
 +
 +The four denizens of Brynn are how the sunsword quest is told: Farmer Jim, the
 +Mayor, Edna and Father Monoly each know a piece of it.
 +
 +==== Step 32. Shopkeepers ====
 +
 +''engine/Shop.java''. Buy, sell, and a shopkeeper who objects when you leave
 +with something you have not paid for. The paged list it uses is
 +''item_pagedisplay'' — **the screen we already built in Step 12's paging**, so
 +this is the shop logic and not the shop interface. ''IT_VALUE'' has been sitting
 +in the item table unread since the import, waiting for exactly this.
 +
 +===== Part X — the quest =====
 +
 +==== Step 33. Croky Castle ====
 +
 +The goal level, and the only one with no stairs down. Reached from ''BDungeon''
 +depth 9, but only while the sunsword has not been found.
 +
 +==== Step 34. The sunsword ====
 +
 +''WeaponData[15]'', probability 0 so it is never generated at random — it is
 +**placed**. Wielding it sets ''GameFlags.has_sunsword'', says //"The sunsword
 +seems to glow and gleam with an unearthly light!"//, and from then on it
 +**attracts monsters**: ''Engine.gametick'' pops an extra one every hundred
 +turns with //"You've got a bad feeling about this..."//
 +
 +The first unique object in the game, and the first item whose being carried
 +changes the rules.
 +
 +==== Step 35. The endgame ====
 +
 +Carry it back to the surface. ''Engine'' checks, every time you take a
 +staircase, whether you have the sunsword and are standing in ''Start'' — and
 +if so, +1000 and //"You have escaped the dungeons of doom!"// That is the win
 +condition, and it is four lines. It needs Step 17's death screen to exist,
 +because winning and dying print the same tombstone.
 +
 +===== Part XI — depth, once it is a game =====
 +
 +==== Step 36. Traps ====
 +
 +Six: trapdoor, bear trap, teleport, dart, sleeping gas, rust. A tile flag, a
 +kind, and a hidden bit.
 +
 +==== Step 37. Search, and secret doors ====
 +
 +''s'', and ''TileData.SECDOOR''. BrynnWell was hand-converted with its secret
 +doors turned into ordinary ones back in Step 8 //because there was no search
 +command//; this is the step that lets them go back.
 +
 +==== Step 38. Experience, and the stat block ====
 +
 +''Stats'': STR, DEX, INT. The ratings are already the right shape and simply
 +have the terms missing — ''getAttackRating()'' is ''10*xp_level + 3*DEX + STR
 ++ bonuses'', and ours is ''player_str'' standing in for all of it. Killing
 +things should raise a level, and the monster spawn window already reads
 +''plevel'' and has been given a hard-coded 1 since Step 18.
 +
 +==== Step 39. LCave, and a third kind of level ====
 +
 +The cave generator, skipped in Step 9. Gives ''BrynnWell'' somewhere to branch
 +to that is not more of the same.
 +
 +==== Step 40. Saving ====
 +
 +''S'' to save and resume, and ''ObjSaver'' for the shape of it. The file
 +services already exist behind ''INT $15''. The queues make this harder than it
 +looks: what is saved is a graph of pointers into a fixed node array, so it
 +saves as indices or not at all.
 +
 +==== Step 41. The rest of the flavour ====
 +
 +''Fortune'' rumours, Edna's pies, the dogfood, the well, the altar and the
 +throne. None of it is systems work; all of it is what makes the place feel
 +like somewhere rather than a grid.
  
 ===== If only three steps get done ===== ===== If only three steps get done =====
Line 856: Line 1459:
 of sight. That is the difference between a demo and a roguelike, and none of of sight. That is the difference between a demo and a roguelike, and none of
 the three is large. the three is large.
 +
 +===== And if only three MORE get done =====
 +
 +**Steps 21, 22 and 29** — names in messages, the effects hook, and branching
 +stairs. The first makes every turn read like a game; the second is what every
 +consumable in the table is waiting for; the third is the shape the whole rest
 +of the world hangs off.
  
sd/rogueima_plan.1788854239.txt.gz · Last modified: by 127.0.0.1

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki