g | x | w | all
Bytes Lang Time Link
408CASIO BASIC CASIO fx9750GIII250204T183351Zmadeforl
251Bash on vt510rm250117T154844ZThemooni
250Java OpenJDK 8250117T143411ZmastaH
173JavaScript ES8250116T204331ZArnauld
080Charcoal250117T012110ZNeil

CASIO BASIC (CASIO fx-9750GIII), 424 408 bytes

I had to replace the characters with some that are close enough, so sorry about that.

"ABCDEFGHIJKLMNOPQRSTUVWXYZ"→Str 2
{80,25→Dim Mat A
" /×☐■"→Str 9
1→Q~R
For 1→N To StrLen(Str 1
StrMid(Str 1,N,1→Str 8
If Not StrCmp("?",Str 8
Then StrMid(Str 1,N+1,1→Str 7
Isz N
Else If Not StrCmp("!",Str 8
Then Mat A[Q,R→Str 7
Mat A[Q,R>9⟹StrMid(Str 2,Mat A[Q,R]-10,1→Str 7
Else If StrSrc("01234",Str 8)
Then Exp(Str 8)+1
Ans→Mat A[Q,R
Text 1+6R,1+6Q,StrMid(Str 9,Ans,1)
IfEnd
R-Not StrCmp("W",Str 8)+(R<1)+Not StrCmp("S",Str 8)-(R>25→R
Q-Not StrCmp("A",Str 8)+(Q<1)+Not StrCmp("D",Str 8)-(Q>80→Q
If Not StrCmp("5",Str 8
Then Text 1+6R,1+6Q,Str 7
10+StrSrc(Str 2,Str 7)→Mat A[Q,R
IfEnd
IfEnd
IfEnd
Next

Bash on vt510-rm, 349 292 251 bytes

-57 bytes by cedilla and myself by factorising the echo calls into a function, and replacing ((y>0))&&z A&&((y--)) with ((y>0&&y--))&&z A, as well as removing unneeded newlines.

-41 bytes by cedilla by further factorising the movements

p()(printf $2\\33[1$1)
m(){(($1<$2&&$3))&&p $4;}
r="read -sn1 k"
b=\ ░▒▓█
s=$b
f()($r&&(case $k in
w)m 0 y y-- A;;s)m y 25 ++y B;;d)m x 80 ++x C;;a)m 0 x x-- D;;[0-5])p D ${s:$k:1};;?)$r;s=$b$k;;!)p ";1;$y;$y;$x;$x*y";$r;s=$b${k:6};;esac
f))
f

Don't try this online, it requires a terminal that supports DECRQCRA. I don't have one, but as far as I can tell, this should work (maybe). DECRQCRA is a vt510-rm escape code that asks the terminal to do a checksum of all the characters in a rectangular area. if you ask it to check an area of 1x1, it returns the sum of 1 character (so that character). It is largely unsupported because that means that applications would be able to read what previous apps launched from the same terminal wrote, and is thus a security risk. For this reasons, command line programs that need to reference what they've previously outputted need to use ncurses or otherwise keep an array of what the ouput looks like, on modern terminals.

only ! requires the exotic terminal, the rest works on normal modern terminals:

test case 1

my shell catches commands that don't end at the beggining of a line (and prints the ugly %), so i've taken the liberty of adding sd to the test case so we could see it in its entirety

! is untested, I can emulate it with ?▒ here: test case 2

test case 3

if you don't pipe in a test case, read will read from stdin, which means you get to paint manually which is actually pretty fun :D enter image description here

Java (OpenJDK 8), 263 261 251 250 bytes

char[]g=new char[2000];Arrays.fill(g,' ');for(int i=0,p=0,c,e=80;i<s.length;)if((c=s[i++])>47&c<54)g[p]=(" ░▒▓█"+s[0]).charAt(c-48);else if(c<e)s[0]=c<34?g[p]:s[i++];else p+=c>100?c>115?p<e?0:-e:p>1975?0:e:c>99?p%e>78?0:1:p%e<1?0:-1;return g;

Try it online!

Commented

char[] p(char[] s) {
    char[] g = new char[2000];          // output 25*80=2000 characters
    Arrays.fill(g, ' ');                // fill with spaces
    for(int p = 0,                      // array (p)osition
            i = 0,                      // input (i)ndex
            c,                          // current (c)haracter
            e = 80;                     // (e)ighty literal
            i < s.length;)              // for each character in input array
        if ((c = s[i++]) > 47 & c < 54) //   if c = "0","1","2","3","4" or "5"
            g[p] = (" ░▒▓█" + s[0])     //     add stored value to grayscale characters
                    .charAt(c - 48);    //     converto to number and get output
        else if (c < e)                 //   if c = "?" or "!"
            s[0] =                      //   store in input array
                c < 34 ?                //     if c = "!"
                    g[p]                //       character at current position
                :                       //     else (c = "?")
                    s[i++];             //       next input character
        else                            //   if c = "w","a","s" or "d"
            p +=                        //     update (p)osition with
                c > 100 ?               //       if c = "w" or "s"
                    c > 115 ?           //         if c = "w" [up]
                        p < e ?         //           if y = 0
                            0           //             don't move
                        :               //           else (y > 0)
                            -e          //             move up by a full row (-80 chars)
                    :                   //         else (c = "s") [down]
                        p > 1975 ?      //           if y = 24 ((25-1)*80=1975)
                            0           //             don't move
                        :               //           else (y < 24)
                            e           //             move down by a full row (+80 chars)
                :                       //       else (c = "a" or "d")
                    c > 99 ?            //         if c = "d" [right]
                        p % e > 78 ?    //           if x = 79
                            0           //             don't move
                        :               //           else (x < 79)
                            1           //             move right
                    :                   //         else (c = "a") [left]
                        p % e < 1 ?     //           if x = 0
                            0           //             don't move
                        :               //           else (x > 0)
                            -1;         //             move left
    return g;                           // return array
}

JavaScript (ES8), 173 bytes

Expects an array of characters and returns a matrix of characters.

a=>a.map(c=>~a?1/c?m[y][x]=" ░▒▓█"[c]||a:c<{}?a=c>"!"?-1:m[y][x]:c>"s"?y-=!!y:c>"d"?y+=y<24:x+=c>"a"?x<79:-!!x:a=c,m=[...1e9+{}].map(_=>[..."".padEnd(80)]),x=y=0)&&m

Try it online!

Commented

a => a.map(c =>           // for each character c in the input array a[]:
  ~a ?                    //   if a is not -1:
    1 / c ?               //     if c is a digit:
      m[y][x] =           //       update m[y][x]:
        " ░▒▓█"[c]        //         with either a 'grayscale' character
        || a              //         or a (if c = 5)
      :                   //     else:
        c < {} ?          //       if c is not a letter:
          a =             //         update a:
            c > "!" ?     //           if c = "?":
              -1          //             use -1
            :             //           else (c = "!"):
              m[y][x]     //             use the character at the current position
        :                 //       else (c is a letter):
          c > "s" ?       //         if c = "w":
            y -= !!y      //           decrement y if it's not 0
          :               //         else:
            c > "d" ?     //           if c = "s":
              y += y < 24 //             increment y if it's less than 24
            :             //           else:
              x +=        //             update x:
                c > "a" ? //               if c = "d":
                  x < 79  //                 increment x if it's less than 79
                :         //               else (c = "a"):
                  -!!x    //                 decrement x if it's not 0
  :                       //   else (a = -1):
    a = c,                //     copy c in a
  m = [...1e9 + {}]       //   start with a matrix m[] made of 25 rows ...
    .map(_ =>             //
      [..."".padEnd(80)]  //     ... of 80 columns, filled with spaces
    ),                    //
  x = y = 0               //   start with x = 0 and y = 0
) && m                    // end of map() -> return m[]

Charcoal, 80 bytes

UO⁸⁰¦²⁵ ≔⪪ ░▒▓█ ¹θFS¿υ§≔θ⊟υι≡ιwM›ⅉ⁰↑aM›ⅈ⁰←sM‹ⅉ²⁴↓dM‹ⅈ⁷⁹→?⊞υ⁵!§≔θ⁵KKP§θIι

Try it online! Link is to verbose version of code. Note that TIO's deverbosifier inserts an unnecessary ”y”. Explanation:

UO⁸⁰¦²⁵ 

Create the grid.

≔⪪ ░▒▓█ ¹θ

Create the list of drawing characters for the commands 0-5. (Note that non-code page characters are allowed but cost 3 or 4 bytes each; this has been included in the byte count.)

FS

Loop over the input characters.

¿υ§≔θ⊟υι

If the last character was a ? then replace the 5 character with the current character.

≡ι

Otherwise, switch on the current character.

wM›ⅉ⁰↑

If it's a w then move up unless the cursor is in the top row.

aM›ⅈ⁰←

If it's an a then move left unless the cursor is in the left column.

sM‹ⅉ²⁴↓

If it's an s then move down unless the cursor is in the bottom row.

dM‹ⅈ⁷⁹→

If it's a d then move right unless the cursor is in the right column.

?⊞υ⁵

If it's a ? then prepare to save the next character as the output for the 5 command.

!§≔θ⁵KK

If it's a ! then save the character under the cursor as the output for the 5 command.

P§θIι

Otherwise replace the character under the cursor with the appropriate character for the commands 0-5.

18 bytes to prevent the cursor from exiting the grid may seem a lot, but trying to share code between the directions also takes 18 bytes, of which 9 is used translating from wasd to Charcoal direction codes which could be removed if uldr could be used instead.